# Timeline 02 - Interactive Timeline

> An engaging timeline layout featuring key milestones, chronological progression, and supporting visuals, crafted to communicate company evolution effectively.

A shadcn/ui block from the [@shadcn-space](https://shadcnspace.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-space/timeline-02).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-space/timeline-02
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-space/timeline-02-light-bf60f38fc6782343.png
- Registry item: https://shadcnspace.com/r/timeline-02.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnspace.com/r/timeline-02.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `badge`, `button`

## Files

### `src/components/shadcn-space/blocks/timeline-02/page.tsx`

```tsx
import TimelineBlock from "@/components/shadcn-space/blocks/timeline-02";

export default function TimelinePage() {
    return <TimelineBlock />
}
```

### `src/components/shadcn-space/blocks/timeline-02/timeline/index.tsx`

```tsx
"use client";

import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

export interface TimelineItem {
    year: string;
    title: string;
    description: string;
    image: string;
    link?: string;
    badge?: string;
}

interface TimelineProps {
    items?: TimelineItem[];
    className?: string;
}

const defaultItems: TimelineItem[] = [
    {
        year: "2011",
        title: "The Beginning",
        description: "Our journey started with a small team and a big vision to revolutionize the digital landscape.",
        image: "https://images.shadcnspace.com/assets/services/services-img-1.webp",
        badge: "Timeline",
    },
    {
        year: "2012",
        title: "First Major Milestone",
        description: "Successfully launched our first flagship product, gaining recognition across the industry.",
        image: "https://images.shadcnspace.com/assets/services/service06-img2.webp",
        badge: "Timeline",
    },
    {
        year: "2013",
        title: "Driving Innovation",
        description: "We introduced modern technologies and creative solutions, helping businesses adapt, evolve, and stay ahead in competitive markets.",
        image: "https://images.shadcnspace.com/assets/services/services-1.png",
        badge: "Timeline",
    },
    {
        year: "2014",
        title: "Global Expansion",
        description: "Expanded our operations to three continents, bringing our solutions to a global audience.",
        image: "https://images.shadcnspace.com/assets/services/service06-img2.webp",
        badge: "Timeline",
    },
    {
        year: "2015",
        title: "Tech Innovation Award",
        description: "Received prestigious awards for our contribution to open-source software and technology.",
        image: "https://images.shadcnspace.com/assets/services/services-08-bg-03.webp",
        badge: "Timeline",
    },
    {
        year: "2016",
        title: "Scaling Up",
        description: "Grew our team to over 100 passionate individuals, fostering a culture of creativity and excellence.",
        image: "https://images.shadcnspace.com/assets/services/services-08-bg-01.webp",
        badge: "Timeline",
    },
];

export const Timeline = ({ items = defaultItems, className }: TimelineProps) => {
    const [activeIndex, setActiveIndex] = useState(2);
    const activeItem = items[activeIndex];
    const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);

    useEffect(() => {
        const activeRef = buttonRefs.current[activeIndex];
        if (activeRef) {
            activeRef.scrollIntoView({
                behavior: "smooth",
                block: "center",
                inline: "center"
            });
        }
    }, [activeIndex]);

    return (
        <div className={cn("w-full", className)}>
            <div className="grid grid-cols-12 gap-6">
                <div className="relative flex flex-col items-center shrink-0 w-full lg:w-auto lg:col-span-1 col-span-12">
                    <div className="flex flex-row lg:flex-col gap-6 lg:h-105 overflow-auto no-scrollbar md:snap-y snap-x lg:py-40 lg:px-0 px-40 snap-mandatory w-full" style={{ scrollBehavior: "smooth" }}>
                        {items.map((item, index) => (
                            <Button
                                variant={activeIndex === index ? "default" : "outline"}
                                key={item.year}
                                ref={(el) => { buttonRefs.current[index] = el; }}
                                onClick={() => setActiveIndex(index)}
                                className={cn("rounded-full px-5! transition-all duration-300 shrink-0 snap-center")}
                            >
                                <span className="text-sm font-medium tracking-tight">
                                    {item.year}
                                </span>
                            </Button>
                        ))}
                    </div>

                    {/* Gradient Overlays */}
                    <div className="absolute inset-x-0 top-0 h-40 bg-linear-to-b from-background to-transparent pointer-events-none z-10 lg:block hidden" />
                    <div className="absolute inset-x-0 bottom-0 h-40 bg-linear-to-t from-background to-transparent pointer-events-none z-10 lg:block hidden" />
                </div>

                <div className="flex-1 flex flex-col lg:flex-row items-center gap-12 lg:gap-24 w-full overflow-hidden lg:col-span-11 col-span-12">
                    <AnimatePresence mode="wait">
                        <motion.div
                            key={activeItem.year}
                            initial={{ opacity: 0, x: 20 }}
                            animate={{ opacity: 1, x: 0 }}
                            exit={{ opacity: 0, x: -20 }}
                            transition={{ duration: 0.4, ease: "easeInOut" }}
                            className="flex-1 grid lg:grid-cols-11 grid-cols-1 items-center gap-6 w-full"
                        >
                            <div className="flex-1 flex flex-col items-start gap-3 xl:ps-10 xl:pe-16 lg:col-span-6">
                                <Badge variant="secondary" className="rounded-full h-6 px-2 py-1 font-normal">
                                    {activeItem.badge}
                                </Badge>
                                <h2 className="text-5xl md:text-5xl lg:text-8xl font-medium tracking-tight text-foreground">
                                    {activeItem.year}
                                </h2>
                                <h3 className="text-2xl font-medium text-foreground leading-tight">
                                    {activeItem.title}
                                </h3>
                                <p className="sm:text-lg text-base text-muted-foreground leading-relaxed max-w-xl">
                                    {activeItem.description}
                                </p>
                            </div>

                            <div className="w-full flex flex-col gap-4 lg:ps-5 lg:col-span-5">
                                <div className="w-full rounded-lg overflow-hidden bg-muted max-h-68 md:max-h-91 aspect-11/9 md:aspect-6/3 lg:aspect-11/9">
                                    <img
                                        src={activeItem.image}
                                        alt={activeItem.title}
                                        className="w-full h-full object-cover object-top"
                                    />
                                </div>

                                {activeItem.link && (
                                    <a href={activeItem.link} target="_blank" className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors group" >
                                        Learn More
                                        <ArrowRight className="w-4 h-4 group-hover:-rotate-45 duration-200 transition-all" />
                                    </a>
                                )}

                                {!activeItem.link && (
                                    <a href="#" className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors text-left group">
                                        Learn More
                                        <ArrowRight className="w-4 h-4 group-hover:-rotate-45 duration-200 transition-all" />
                                    </a>
                                )}
                            </div>
                        </motion.div>
                    </AnimatePresence>
                </div>
            </div>
        </div >
    );
};

export default Timeline;
```

### `src/components/shadcn-space/blocks/timeline-02/index.tsx`

```tsx
import Timeline from "@/components/shadcn-space/blocks/timeline-02/timeline";

const TimelineBlock = () => {
    return (
        <section className="py-24 bg-background overflow-hidden">
            <div className="max-w-7xl mx-auto px-4 md:px-8 lg:px-16">
                <Timeline />
            </div>
        </section>
    );
};

export default TimelineBlock;
```
