# Arc Scroll: Timeline

> A company history with tighter spacing, the current year highlighted and a year rail that follows along.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/arc-scroll-timeline-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/arc-scroll-timeline-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/arc-scroll-timeline-demo-light-87da08c31fdc684d.png
- Registry item: https://keyframeui.com/r/arc-scroll-timeline-demo.json
- Categories: scroll, cards, motion
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/arc-scroll-timeline-demo.json
```

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

## Dependencies

- Registry items: `https://keyframeui.com/r/arc-scroll.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/arc-scroll#arc-scroll-timeline-demo

## Files

### `registry/demos/arc-scroll-timeline-demo.tsx`

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";

import { cn } from "@/lib/utils";
import { ArcScroll } from "@/components/ui/arc-scroll";

const milestones = [
  { year: 2019, label: "Founded", detail: "Two designers, a spare bedroom and a prototype." },
  { year: 2020, label: "Beta", detail: "Version 0.1 ships to forty teams who said yes." },
  { year: 2021, label: "Seed", detail: "A seed round grows the team to twelve." },
  { year: 2022, label: "1.0", detail: "A stable API and our first paying customers." },
  { year: 2023, label: "10k teams", detail: "Ten thousand teams ship with us every week." },
  { year: 2024, label: "Global", detail: "New offices in Berlin and Singapore." },
  { year: 2025, label: "Platform", detail: "Plugins open to third-party developers." },
  { year: 2026, label: "Today", detail: "Still small, still shipping every week." },
];

export function ArcScrollTimelineDemo() {
  const scrollRef = React.useRef<HTMLDivElement>(null);
  const [active, setActive] = React.useState(0);
  const reduceMotion = useReducedMotion();
  const milestone = milestones[active];

  return (
    <div className="relative w-full overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950">
      <div ref={scrollRef} className="relative h-[var(--demo-height,34rem)] overflow-y-auto overscroll-contain">
        <ArcScroll
          containerRef={scrollRef}
          stageClassName="h-[var(--demo-height,34rem)]"
          spacing={12}
          scrollPerCard={220}
          onActiveChange={setActive}
          overlay={
            <div className="absolute inset-x-0 bottom-8 flex flex-col items-center gap-6 px-6 text-center">
              <motion.p
                key={milestone.year}
                initial={{ opacity: 0, y: reduceMotion ? 0 : 6 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ duration: 0.25, ease: "easeOut" }}
                className="max-w-xs text-sm text-zinc-600 dark:text-zinc-400"
              >
                {milestone.detail}
              </motion.p>
              <ol className="flex gap-1 font-mono text-[11px] tabular-nums">
                {milestones.map((item, index) => (
                  <li
                    key={item.year}
                    className={cn(
                      "relative px-1.5 py-1 transition-colors duration-200",
                      index === active
                        ? "text-zinc-900 dark:text-zinc-50"
                        : "text-zinc-400 dark:text-zinc-600",
                    )}
                  >
                    {index === active ? (
                      <motion.span
                        layoutId="arc-timeline-tick"
                        transition={{ type: "spring", stiffness: 500, damping: 40 }}
                        className="absolute inset-x-1.5 -bottom-0.5 h-px bg-current"
                      />
                    ) : null}
                    {String(item.year).slice(2)}
                  </li>
                ))}
              </ol>
            </div>
          }
        >
          {milestones.map((item, index) => (
            <div
              key={item.year}
              className={cn(
                "flex h-40 w-28 flex-col justify-between rounded-2xl p-3 shadow-lg ring-1 transition-colors duration-300 sm:w-32",
                index === active
                  ? "bg-zinc-900 text-zinc-50 shadow-zinc-900/20 ring-zinc-900 dark:bg-zinc-50 dark:text-zinc-900 dark:ring-zinc-50"
                  : "bg-white text-zinc-900 shadow-zinc-900/5 ring-zinc-200 dark:bg-zinc-900 dark:text-zinc-50 dark:shadow-black/40 dark:ring-zinc-800",
              )}
            >
              <span className="font-mono text-[10px] uppercase tracking-widest opacity-60">
                {item.label}
              </span>
              <span className="text-3xl font-semibold tracking-tight tabular-nums">
                {item.year}
              </span>
            </div>
          ))}
        </ArcScroll>
      </div>
    </div>
  );
}
```
