Skip to content
Dethink Components

ComponentsTimeline

Timeline

Show events in order with statuses and optional selection.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • Lucide (lucide-react)

    React SVG icons used in component controls and status indicators.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

Import the component into your page or component file.

Usage
import {
  Timeline,
  type TimelineItemData,
} from "@dethink/components";

Try the examples, then open the code to use them in your app. Interactive timelines support arrow keys and Home/End. Use flow for readable histories and canvas when people need to explore a large track.

Activity history

Dated milestones with complete, current, and upcoming statuses.

  1. Status: Complete

    Research complete

    User needs and interaction model approved.

  2. Status: Current

    Private beta

    First teams using the component in dashboards.

  3. Status: Upcoming

    General availability

    Registry item, docs, and release notes ship.

Show sourceexamples/timeline/basic.tsx
examples/timeline/basic.tsx
"use client";

import { Timeline, type TimelineItemData } from "@dethink/components";

const milestones: TimelineItemData[] = [
  {
    id: "research",
    title: "Research complete",
    description: "User needs and interaction model approved.",
    datetime: "2026-01-12T09:00:00Z",
    dateLabel: "Jan 12",
    status: "complete",
  },
  {
    id: "beta",
    title: "Private beta",
    description: "First teams using the component in dashboards.",
    datetime: "2026-03-18T10:00:00Z",
    dateLabel: "Mar 18",
    status: "current",
  },
  {
    id: "ga",
    title: "General availability",
    description: "Registry item, docs, and release notes ship.",
    datetime: "2026-05-05T10:00:00Z",
    dateLabel: "May 5",
    status: "upcoming",
  },
];

export function TimelineBasic() {
  return (
    <Timeline
      interactive={false}
      aria-label="Release milestones"
      items={milestones}
    />
  );
}

Deployment progress

A readable sequence of completed, current and upcoming stages.

  1. Status: Complete

    Queued

    Waiting for a build slot.

  2. Status: Current

    Building

    Compiling and running checks.

  3. Status: Upcoming

    Deploying

    Rolling out to the edge.

  4. Status: Upcoming

    Live

    Serving production traffic.

Show sourceexamples/timeline/progress.tsx
examples/timeline/progress.tsx
"use client";

import { Timeline, type TimelineItemData } from "@dethink/components";

const steps: TimelineItemData[] = [
  {
    id: "queued",
    title: "Queued",
    description: "Waiting for a build slot.",
    status: "complete",
  },
  {
    id: "building",
    title: "Building",
    description: "Compiling and running checks.",
    status: "current",
  },
  {
    id: "deploying",
    title: "Deploying",
    description: "Rolling out to the edge.",
    status: "upcoming",
  },
  {
    id: "live",
    title: "Live",
    description: "Serving production traffic.",
    status: "upcoming",
  },
];

export function TimelineProgress() {
  return (
    <Timeline
      interactive={false}
      aria-label="Deployment progress"
      mode="progress"
      scale="auto"
      layout="stacked"
      items={steps}
    />
  );
}

Grouped release activity

Scan by date, then expand an event for its owner and commit. Details do not change selection.

  1. Status: Complete
    Sep 21 · 16:20

    Release approved

    Review complete. Production rollout scheduled.

  2. Status: Complete
    Sep 22 · 09:02

    Build verified

    All checks passed across the release targets.

  3. Status: Warning
    Sep 22 · 09:06

    Canary needs attention

    Latency is above the rollout threshold.

  4. Status: Current
    Sep 22 · 09:12

    Production rollout

    Monitoring the first production region.

Show sourceexamples/timeline/enhanced.tsx
examples/timeline/enhanced.tsx
"use client";

import { useState } from "react";
import {
  Button,
  Timeline,
  TimelineFeed,
  type TimelineItemData,
} from "@dethink/components";

type RunData = { day: string; author: string; commit: string };
const history: TimelineItemData<RunData>[] = [
  {
    id: "plan",
    title: "Release approved",
    dateLabel: "Sep 21 · 16:20",
    status: "complete",
    data: { day: "September 21", author: "Dana", commit: "a92b641" },
    description: "Review complete. Production rollout scheduled.",
  },
  {
    id: "build",
    title: "Build verified",
    dateLabel: "Sep 22 · 09:02",
    status: "complete",
    data: { day: "September 22", author: "Miguel", commit: "f642257" },
    description: "All checks passed across the release targets.",
  },
  {
    id: "canary",
    title: "Canary needs attention",
    dateLabel: "Sep 22 · 09:06",
    status: "warning",
    data: { day: "September 22", author: "Priya", commit: "f642257" },
    description: "Latency is above the rollout threshold.",
  },
  {
    id: "rollout",
    title: "Production rollout",
    dateLabel: "Sep 22 · 09:12",
    status: "current",
    data: { day: "September 22", author: "Dana", commit: "f642257" },
    description: "Monitoring the first production region.",
  },
];

export function TimelineGroupedDetails() {
  return (
    <Timeline
      aria-label="Grouped release activity"
      items={history}
      interactive={false}
      getGroup={(item) =>
        item.data ? { id: item.data.day, label: item.data.day } : null
      }
      renderDetails={(item) => (
        <div className="space-y-2">
          <p>Owner: {item.data?.author}</p>
          <p>
            Commit: <code>{item.data?.commit}</code>
          </p>
          <Button
            variant="outline"
            size="sm"
            onClick={() => {
              void navigator.clipboard.writeText(item.data?.commit ?? "");
            }}
          >
            Copy commit
          </Button>
        </div>
      )}
    />
  );
}

const initialFeed: TimelineItemData[] = Array.from(
  { length: 8 },
  (_, index) => ({
    id: `event-${index}`,
    title: `Check ${index + 1} completed`,
    description: "Release checks recorded and ready for review.",
    dateLabel: `09:${String(index).padStart(2, "0")}`,
    status: "complete",
  }),
);
export function TimelineLiveFeed() {
  const [items, setItems] = useState(initialFeed);
  return (
    <div className="w-full space-y-4">
      <div className="flex flex-wrap gap-2">
        <Button
          size="sm"
          onClick={() =>
            setItems((current) => [
              ...current,
              {
                id: `event-${current.length}`,
                title: `Check ${current.length + 1} completed`,
                description: "A new result arrived from the release agent.",
                dateLabel: "Just now",
                status: "complete",
              },
            ])
          }
        >
          Add event
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() =>
            setItems((current) => [
              {
                id: `older-${current.length}`,
                title: "Earlier release note",
                description:
                  "An older event loaded without adding to the unread count.",
                dateLabel: "Earlier",
                status: "neutral",
              },
              ...current,
            ])
          }
        >
          Load earlier example
        </Button>
      </div>
      <TimelineFeed
        aria-label="Live release activity"
        items={items}
        interactive={false}
        reveal="stagger"
      />
    </div>
  );
}

export function TimelineCards() {
  return (
    <Timeline
      aria-label="Release cards"
      items={history.slice(1, 3)}
      variant="cards"
      interactive={false}
    />
  );
}

Live release feed

Scroll up, then add an event. Your reading position stays put until you choose Jump to latest.

  1. Status: Complete
    09:00

    Check 1 completed

    Release checks recorded and ready for review.

  2. Status: Complete
    09:01

    Check 2 completed

    Release checks recorded and ready for review.

  3. Status: Complete
    09:02

    Check 3 completed

    Release checks recorded and ready for review.

  4. Status: Complete
    09:03

    Check 4 completed

    Release checks recorded and ready for review.

  5. Status: Complete
    09:04

    Check 5 completed

    Release checks recorded and ready for review.

  6. Status: Complete
    09:05

    Check 6 completed

    Release checks recorded and ready for review.

  7. Status: Complete
    09:06

    Check 7 completed

    Release checks recorded and ready for review.

  8. Status: Complete
    09:07

    Check 8 completed

    Release checks recorded and ready for review.

Show sourceexamples/timeline/enhanced.tsx
examples/timeline/enhanced.tsx
"use client";

import { useState } from "react";
import {
  Button,
  Timeline,
  TimelineFeed,
  type TimelineItemData,
} from "@dethink/components";

type RunData = { day: string; author: string; commit: string };
const history: TimelineItemData<RunData>[] = [
  {
    id: "plan",
    title: "Release approved",
    dateLabel: "Sep 21 · 16:20",
    status: "complete",
    data: { day: "September 21", author: "Dana", commit: "a92b641" },
    description: "Review complete. Production rollout scheduled.",
  },
  {
    id: "build",
    title: "Build verified",
    dateLabel: "Sep 22 · 09:02",
    status: "complete",
    data: { day: "September 22", author: "Miguel", commit: "f642257" },
    description: "All checks passed across the release targets.",
  },
  {
    id: "canary",
    title: "Canary needs attention",
    dateLabel: "Sep 22 · 09:06",
    status: "warning",
    data: { day: "September 22", author: "Priya", commit: "f642257" },
    description: "Latency is above the rollout threshold.",
  },
  {
    id: "rollout",
    title: "Production rollout",
    dateLabel: "Sep 22 · 09:12",
    status: "current",
    data: { day: "September 22", author: "Dana", commit: "f642257" },
    description: "Monitoring the first production region.",
  },
];

export function TimelineGroupedDetails() {
  return (
    <Timeline
      aria-label="Grouped release activity"
      items={history}
      interactive={false}
      getGroup={(item) =>
        item.data ? { id: item.data.day, label: item.data.day } : null
      }
      renderDetails={(item) => (
        <div className="space-y-2">
          <p>Owner: {item.data?.author}</p>
          <p>
            Commit: <code>{item.data?.commit}</code>
          </p>
          <Button
            variant="outline"
            size="sm"
            onClick={() => {
              void navigator.clipboard.writeText(item.data?.commit ?? "");
            }}
          >
            Copy commit
          </Button>
        </div>
      )}
    />
  );
}

const initialFeed: TimelineItemData[] = Array.from(
  { length: 8 },
  (_, index) => ({
    id: `event-${index}`,
    title: `Check ${index + 1} completed`,
    description: "Release checks recorded and ready for review.",
    dateLabel: `09:${String(index).padStart(2, "0")}`,
    status: "complete",
  }),
);
export function TimelineLiveFeed() {
  const [items, setItems] = useState(initialFeed);
  return (
    <div className="w-full space-y-4">
      <div className="flex flex-wrap gap-2">
        <Button
          size="sm"
          onClick={() =>
            setItems((current) => [
              ...current,
              {
                id: `event-${current.length}`,
                title: `Check ${current.length + 1} completed`,
                description: "A new result arrived from the release agent.",
                dateLabel: "Just now",
                status: "complete",
              },
            ])
          }
        >
          Add event
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() =>
            setItems((current) => [
              {
                id: `older-${current.length}`,
                title: "Earlier release note",
                description:
                  "An older event loaded without adding to the unread count.",
                dateLabel: "Earlier",
                status: "neutral",
              },
              ...current,
            ])
          }
        >
          Load earlier example
        </Button>
      </div>
      <TimelineFeed
        aria-label="Live release activity"
        items={items}
        interactive={false}
        reveal="stagger"
      />
    </div>
  );
}

export function TimelineCards() {
  return (
    <Timeline
      aria-label="Release cards"
      items={history.slice(1, 3)}
      variant="cards"
      interactive={false}
    />
  );
}

Bordered cards

Use cards when events benefit from more separation.

  1. Status: Complete
    Sep 22 · 09:02

    Build verified

    All checks passed across the release targets.

  2. Status: Warning
    Sep 22 · 09:06

    Canary needs attention

    Latency is above the rollout threshold.

Show sourceexamples/timeline/enhanced.tsx
examples/timeline/enhanced.tsx
"use client";

import { useState } from "react";
import {
  Button,
  Timeline,
  TimelineFeed,
  type TimelineItemData,
} from "@dethink/components";

type RunData = { day: string; author: string; commit: string };
const history: TimelineItemData<RunData>[] = [
  {
    id: "plan",
    title: "Release approved",
    dateLabel: "Sep 21 · 16:20",
    status: "complete",
    data: { day: "September 21", author: "Dana", commit: "a92b641" },
    description: "Review complete. Production rollout scheduled.",
  },
  {
    id: "build",
    title: "Build verified",
    dateLabel: "Sep 22 · 09:02",
    status: "complete",
    data: { day: "September 22", author: "Miguel", commit: "f642257" },
    description: "All checks passed across the release targets.",
  },
  {
    id: "canary",
    title: "Canary needs attention",
    dateLabel: "Sep 22 · 09:06",
    status: "warning",
    data: { day: "September 22", author: "Priya", commit: "f642257" },
    description: "Latency is above the rollout threshold.",
  },
  {
    id: "rollout",
    title: "Production rollout",
    dateLabel: "Sep 22 · 09:12",
    status: "current",
    data: { day: "September 22", author: "Dana", commit: "f642257" },
    description: "Monitoring the first production region.",
  },
];

export function TimelineGroupedDetails() {
  return (
    <Timeline
      aria-label="Grouped release activity"
      items={history}
      interactive={false}
      getGroup={(item) =>
        item.data ? { id: item.data.day, label: item.data.day } : null
      }
      renderDetails={(item) => (
        <div className="space-y-2">
          <p>Owner: {item.data?.author}</p>
          <p>
            Commit: <code>{item.data?.commit}</code>
          </p>
          <Button
            variant="outline"
            size="sm"
            onClick={() => {
              void navigator.clipboard.writeText(item.data?.commit ?? "");
            }}
          >
            Copy commit
          </Button>
        </div>
      )}
    />
  );
}

const initialFeed: TimelineItemData[] = Array.from(
  { length: 8 },
  (_, index) => ({
    id: `event-${index}`,
    title: `Check ${index + 1} completed`,
    description: "Release checks recorded and ready for review.",
    dateLabel: `09:${String(index).padStart(2, "0")}`,
    status: "complete",
  }),
);
export function TimelineLiveFeed() {
  const [items, setItems] = useState(initialFeed);
  return (
    <div className="w-full space-y-4">
      <div className="flex flex-wrap gap-2">
        <Button
          size="sm"
          onClick={() =>
            setItems((current) => [
              ...current,
              {
                id: `event-${current.length}`,
                title: `Check ${current.length + 1} completed`,
                description: "A new result arrived from the release agent.",
                dateLabel: "Just now",
                status: "complete",
              },
            ])
          }
        >
          Add event
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() =>
            setItems((current) => [
              {
                id: `older-${current.length}`,
                title: "Earlier release note",
                description:
                  "An older event loaded without adding to the unread count.",
                dateLabel: "Earlier",
                status: "neutral",
              },
              ...current,
            ])
          }
        >
          Load earlier example
        </Button>
      </div>
      <TimelineFeed
        aria-label="Live release activity"
        items={items}
        interactive={false}
        reveal="stagger"
      />
    </div>
  );
}

export function TimelineCards() {
  return (
    <Timeline
      aria-label="Release cards"
      items={history.slice(1, 3)}
      variant="cards"
      interactive={false}
    />
  );
}

Horizontal timeline

Arrange milestones from left to right with presentation="canvas" and orientation="horizontal". Drag the track, use Fit to see the sequence, or focus Select and navigate with Left/Right and Home/End.

Drag the track · Arrow keys to navigate

  1. Status: Complete
    September 21

    Planning complete

    Scope agreed and the release scheduled.

  2. Status: Complete
    September 22

    Build verified

    Tests passed across all release targets.

  3. Status: Current
    September 23

    Production rollout

    Monitoring the first production region.

  4. Status: Upcoming
    September 24

    Release review

    Review reliability and customer feedback.

Show sourceexamples/timeline/horizontal.tsx
examples/timeline/horizontal.tsx
"use client";

import { Timeline, type TimelineItemData } from "@dethink/components";

const milestones: TimelineItemData[] = [
  {
    id: "planning",
    title: "Planning complete",
    dateLabel: "September 21",
    description: "Scope agreed and the release scheduled.",
    status: "complete",
  },
  {
    id: "build",
    title: "Build verified",
    dateLabel: "September 22",
    description: "Tests passed across all release targets.",
    status: "complete",
  },
  {
    id: "rollout",
    title: "Production rollout",
    dateLabel: "September 23",
    description: "Monitoring the first production region.",
    status: "current",
  },
  {
    id: "review",
    title: "Release review",
    dateLabel: "September 24",
    description: "Review reliability and customer feedback.",
    status: "upcoming",
  },
];

export function TimelineHorizontal() {
  return (
    <Timeline
      aria-label="Horizontal release milestones"
      items={milestones}
      presentation="canvas"
      orientation="horizontal"
      layout="rail"
      scale="sequence"
      viewport={{ controlsVisibility: "always" }}
    />
  );
}

Flow presentation with reveal

Milestones enter as you reach them. Reduced-motion users see the content immediately.

  1. Status: Complete

    Project kickoff

    Scope agreed and delivery squad assembled.

  2. Status: Complete

    Design review

    Flows approved with two accessibility follow-ups.

  3. Status: Current

    Build sprint

    Core screens implemented behind a feature flag.

  4. Status: Upcoming

    Launch

    Flag removed and rollout announced.

Show sourceexamples/timeline/flow-reveal.tsx
examples/timeline/flow-reveal.tsx
"use client";

import { Timeline, type TimelineItemData } from "@dethink/components";

const milestones: TimelineItemData[] = [
  {
    id: "kickoff",
    title: "Project kickoff",
    description: "Scope agreed and delivery squad assembled.",
    datetime: "2026-02-02T09:00:00Z",
    dateLabel: "Feb 2",
    status: "complete",
  },
  {
    id: "design",
    title: "Design review",
    description: "Flows approved with two accessibility follow-ups.",
    datetime: "2026-03-09T14:00:00Z",
    dateLabel: "Mar 9",
    status: "complete",
  },
  {
    id: "build",
    title: "Build sprint",
    description: "Core screens implemented behind a feature flag.",
    datetime: "2026-04-20T10:00:00Z",
    dateLabel: "Apr 20",
    status: "current",
  },
  {
    id: "launch",
    title: "Launch",
    description: "Flag removed and rollout announced.",
    datetime: "2026-06-01T10:00:00Z",
    dateLabel: "Jun 1",
    status: "upcoming",
  },
];

/**
 * presentation="flow" renders the same events data as a static document-flow
 * list — no pan/zoom viewport — and reveal="stagger" animates the items in
 * one by one as the timeline scrolls into view. Reduced-motion users see
 * every item immediately.
 */
export function TimelineFlowReveal() {
  return (
    <Timeline
      interactive={false}
      aria-label="Project milestones"
      items={milestones}
      presentation="flow"
      reveal="stagger"
      revealOptions={{ trigger: "in-view" }}
    />
  );
}

Streaming reveal

Appending to the items array animates only the new points in — built for live sources like agent runs, deploy logs, or activity feeds.

  1. Status: Complete
    12:04:01

    Run queued

    Agent run accepted and scheduled.

  2. Status: Complete
    12:04:07

    Plan drafted

    Task broken into three tool calls.

Show sourceexamples/timeline/reveal-streaming.tsx
examples/timeline/reveal-streaming.tsx
"use client";

import { useState } from "react";
import { Button, Timeline, type TimelineItemData } from "@dethink/components";

const runEvents: TimelineItemData[] = [
  {
    id: "queued",
    title: "Run queued",
    description: "Agent run accepted and scheduled.",
    dateLabel: "12:04:01",
    status: "complete",
  },
  {
    id: "plan",
    title: "Plan drafted",
    description: "Task broken into three tool calls.",
    dateLabel: "12:04:07",
    status: "complete",
  },
  {
    id: "search",
    title: "Search executed",
    description: "Indexed 42 documents, 6 strong matches.",
    dateLabel: "12:04:18",
    status: "complete",
  },
  {
    id: "draft",
    title: "Draft generated",
    description: "Summary produced from the top matches.",
    dateLabel: "12:04:31",
    status: "current",
  },
  {
    id: "review",
    title: "Awaiting review",
    description: "Draft handed off for human approval.",
    dateLabel: "12:04:32",
    status: "upcoming",
  },
];

/**
 * Growing the items array appends points live: only the new items animate in,
 * so a streaming source — an agent run, a deploy log, a support thread — can
 * push events onto the timeline one at a time.
 */
export function TimelineRevealStreaming() {
  const [count, setCount] = useState(2);
  const done = count >= runEvents.length;

  return (
    <div className="space-y-4">
      <div className="flex justify-center gap-2">
        <Button
          size="sm"
          disabled={done}
          onClick={() => setCount((current) => current + 1)}
        >
          Log next event
        </Button>
        <Button size="sm" variant="outline" onClick={() => setCount(2)}>
          Reset
        </Button>
      </div>
      <Timeline
        interactive={false}
        aria-label="Agent run events"
        items={runEvents.slice(0, count)}
        presentation="flow"
        reveal="stagger"
      />
    </div>
  );
}

Examples that combine components for common tasks.

LLM story

A publication-style vertical history showing how LLMs moved from research architecture to everyday product workflows.

  1. Status: Complete

    Attention changes the map

    The Transformer made attention the core primitive, giving language models a cleaner way to learn context at scale.

  2. Status: Complete

    Scale becomes the story

    Large pretrained models showed that more data, compute, and parameters could unlock useful few-shot behavior.

  3. Status: Complete

    Chat becomes the interface

    Conversational assistants made LLMs feel less like research demos and more like everyday software.

  4. Status: Current

    Models become teammates

    LLMs now read, write, see, call tools, and coordinate multi-step work across product workflows.

Show sourceexamples/timeline/recipe-origin-story.tsx
examples/timeline/recipe-origin-story.tsx
"use client";

import { Timeline, type TimelineItemData } from "@dethink/components";

type OriginMilestone = {
  year: string;
};

const originMilestones: TimelineItemData<OriginMilestone>[] = [
  {
    id: "transformer",
    title: "Attention changes the map",
    description:
      "The Transformer made attention the core primitive, giving language models a cleaner way to learn context at scale.",
    datetime: "2017-01-01T00:00:00Z",
    dateLabel: "2017",
    status: "complete",
    marker: <span className="font-mono text-[10px] font-bold">T</span>,
    data: { year: "2017" },
  },
  {
    id: "scale",
    title: "Scale becomes the story",
    description:
      "Large pretrained models showed that more data, compute, and parameters could unlock useful few-shot behavior.",
    datetime: "2020-01-01T00:00:00Z",
    dateLabel: "2020",
    status: "complete",
    marker: <span className="font-mono text-[10px] font-bold">S</span>,
    data: { year: "2020" },
  },
  {
    id: "chat",
    title: "Chat becomes the interface",
    description:
      "Conversational assistants made LLMs feel less like research demos and more like everyday software.",
    datetime: "2022-01-01T00:00:00Z",
    dateLabel: "2022",
    status: "complete",
    marker: <span className="font-mono text-[10px] font-bold">C</span>,
    data: { year: "2022" },
  },
  {
    id: "agents",
    title: "Models become teammates",
    description:
      "LLMs now read, write, see, call tools, and coordinate multi-step work across product workflows.",
    datetime: "2026-01-01T00:00:00Z",
    dateLabel: "2026",
    status: "current",
    marker: <span className="font-mono text-[10px] font-bold">A</span>,
    data: { year: "2026" },
  },
];

export function TimelineRecipeOriginStory() {
  return (
    <Timeline<OriginMilestone>
      aria-label="Story of LLMs"
      items={originMilestones}
      mode="story"
    />
  );
}

Deployment history

Statuses carry rollout health — including a rollback marked error — and controlled selection drives a details panel showing each deployment's commit and author from its typed payload.

  1. Status: Complete

    v1.4.0 → production

    Date suite components released.

  2. Status: Warning

    v1.4.1 → canary

    Latency regression flagged on p99.

  3. Status: Error

    v1.4.1 → production

    Rolled back after checkout error spike.

  4. Status: Complete

    v1.4.2 → production

    Hotfix for date parsing in Safari.

v1.4.1 → production

Commit 38385e3 · deployed by Miguel

Show sourceexamples/timeline/recipe-deploy-history.tsx
examples/timeline/recipe-deploy-history.tsx
"use client";

import { useState } from "react";
import { Timeline, type TimelineItemData } from "@dethink/components";

type DeployPayload = { sha: string; author: string };

const deploys: TimelineItemData<DeployPayload>[] = [
  {
    id: "d-2214",
    title: "v1.4.2 → production",
    description: "Hotfix for date parsing in Safari.",
    datetime: "2026-07-04T08:41:00Z",
    dateLabel: "Today 08:41",
    status: "complete",
    data: { sha: "f642257", author: "Dana" },
  },
  {
    id: "d-2213",
    title: "v1.4.1 → production",
    description: "Rolled back after checkout error spike.",
    datetime: "2026-07-03T16:20:00Z",
    dateLabel: "Yesterday 16:20",
    status: "error",
    data: { sha: "38385e3", author: "Miguel" },
  },
  {
    id: "d-2212",
    title: "v1.4.1 → canary",
    description: "Latency regression flagged on p99.",
    datetime: "2026-07-03T14:05:00Z",
    dateLabel: "Yesterday 14:05",
    status: "warning",
    data: { sha: "38385e3", author: "Miguel" },
  },
  {
    id: "d-2211",
    title: "v1.4.0 → production",
    description: "Date suite components released.",
    datetime: "2026-07-01T11:32:00Z",
    dateLabel: "Jul 1 11:32",
    status: "complete",
    data: { sha: "17f7ab3", author: "Priya" },
  },
];

/**
 * An interactive deployment history: statuses carry the health of each
 * rollout, and selecting an event surfaces its payload — commit and author —
 * in a details panel driven by the controlled selection.
 */
export function TimelineRecipeDeployHistory() {
  const [selectedId, setSelectedId] = useState<string | null>("d-2213");
  const selected = deploys.find((deploy) => deploy.id === selectedId);

  return (
    <div className="space-y-4">
      <Timeline
        aria-label="Deployment history"
        items={deploys}
        selectedId={selectedId}
        onSelectedIdChange={setSelectedId}
      />
      <div
        aria-live="polite"
        className="border-border mx-auto max-w-md rounded-lg border px-4 py-3 text-sm"
      >
        {selected ? (
          <>
            <p className="font-medium">{selected.title}</p>
            <p className="text-muted-foreground mt-0.5">
              Commit <span className="font-mono">{selected.data?.sha}</span> ·
              deployed by {selected.data?.author}
            </p>
          </>
        ) : (
          <p className="text-muted-foreground">
            Select a deployment for details.
          </p>
        )}
      </div>
    </div>
  );
}

Timeline now defaults to a vertical activity list. Set presentation="canvas" to retain pan and zoom, or variant="cards" for bordered flow cards. Reveal defaults are 220ms with a 60ms interval; pass revealOptions to keep your previous timing.

Grouping and expandable details are flow features. Custom renderers remain supported; selection and disclosure controls are separate from links and actions. TimelineFeed installs separately as timeline-feed and owns its scroll panel.

Timeline is data-driven: items carry the content, and the component handles layout, scale, and interaction.

Timeline props
PropWhat it doesDefault
variant"activity" | "cards"Compact activity rows or bordered cards in non-story flow layouts."activity"
getGroup(item) => { id: string; label: ReactNode } | nullLabels contiguous groups after ordering, without reordering events. Flow only.Not set
details / renderDetailsReactNode / TimelineItemRendererItem details or a typed details renderer, exposed through a separate disclosure. Flow only.Not set
expandedIds / defaultExpandedIds / onExpandedIdsChangestring[] / string[] / (ids) => voidControlled or uncontrolled expansion; multiple events may be open.[]
TimelineFeed: followLatest / viewportClassNameboolean / stringContained live feed. Following pauses away from the end; the viewport class customizes its height.true / h-[28rem]
itemsTimelineItemData[]Event data: id, title, description, datetime/dateLabel, status, optional marker and typed payload via data.Not set
mode"events" | "progress" | "story"Dated event history, undated progress sequence, or static editorial story timeline."events"
status (per item)"neutral" | "complete" | "current" | "upcoming" | "warning" | "error"Health/progress tone of each item's marker and card."neutral"
orientation / layout"horizontal" | "vertical" / layout variantsAxis of the track and how item cards stack around it.vertical (flow), horizontal (canvas)
scale / ordertime scale / chronological orderHow datetimes map to track distance and which direction time flows.auto
selectedId / defaultSelectedId / onSelectedIdChangestring | null / string | null / (id) => voidControlled or uncontrolled selection; arrow keys move between items.Not set
presentation"canvas" | "flow"Pannable/zoomable plane, or a static document-flow list with markers, rail, and compact cards."flow"
reveal / revealOptions"none" | "stagger" | "all" / trigger, interval, duration, initialDelayAnimated item entrance for the flow presentation — staggered or all at once, on mount, in view, or manually."none"
revealCount / onItemReveal / onRevealCompletenumber / (id, index) => void / () => voidControlled reveal progression for the manual trigger, plus per-item and completion callbacks. Appended items animate in as their own batch.Not set
interactivebooleanDisables selection for purely presentational timelines.true
viewportTimelineViewportOptionsZoom and pan options for long histories (defaultZoom, limits).Not set
renderItemTimelineItemRendererCustom item rendering with access to the typed payload.built-in card