# Lifecycle Timeline (`lifecycle-timeline`) - SignalOS widget

> Vertical connected event timeline showing an elapsed T+ offset from the first event plus an absolute timestamp. Per-event icon, label, and accent resolve from a caller-supplied style map; detail bodies render via a render prop. Domain-agnostic, with empty and loading states.

- **Version:** 0.1.1
- **Kind:** widget · **Category:** data-display
- **Install:** `npx shadcn@latest add @signalos/lifecycle-timeline`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/skeleton
- **npm dependencies:** lucide-react@^1.7.0
- **Files installed:** `src/components/widgets/lifecycle-timeline/LifecycleTimeline.tsx`, `src/components/widgets/lifecycle-timeline/LifecycleTimeline.types.ts`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Usage

```tsx
import { LifecycleTimeline } from "@/components/widgets/lifecycle-timeline/LifecycleTimeline"
```

## Example

```tsx
// Example: a lifecycle timeline with per-kind styles and custom detail bodies.
import {
  ArrowLeftRight,
  CheckCircle2,
  Radio,
  Sparkles,
  Zap,
} from "lucide-react"

import { LifecycleTimeline } from "@/components/widgets/lifecycle-timeline/LifecycleTimeline"
import type { LifecycleEventStyle } from "@/components/widgets/lifecycle-timeline/LifecycleTimeline.types"

const EVENT_STYLES: Record<string, LifecycleEventStyle> = {
  "signal.fired": {
    icon: Zap,
    label: "Signal fired",
    iconTone: "text-warning-500",
    chipTone: "bg-warning-500/15 text-warning-600 dark:text-warning-400",
  },
  "hypothesis.formed": {
    icon: Sparkles,
    label: "Hypothesis formed",
    iconTone: "text-info-500",
    chipTone: "bg-info-500/15 text-info-500",
  },
  "hypothesis.routed": {
    icon: Radio,
    label: "Routed",
    iconTone: "text-primary",
    chipTone: "bg-primary/15 text-primary",
  },
  "slot.transferred": {
    icon: ArrowLeftRight,
    label: "Transferred",
    iconTone: "text-warning-500",
    chipTone: "bg-warning-500/15 text-warning-600 dark:text-warning-400",
  },
  "decision.created": {
    icon: CheckCircle2,
    label: "Decision",
    iconTone: "text-success-500",
    chipTone: "bg-success-500/15 text-success-600 dark:text-success-400",
  },
}

export default function Example() {
  return (
    <div className="max-w-md rounded-md border border-border/60 bg-card px-3 py-2">
      <LifecycleTimeline
        eventStyles={EVENT_STYLES}
        events={[
          {
            at: "2026-06-09T11:56:40Z",
            kind: "signal.fired",
            detail: "coverage_rate · 3.2σ",
          },
          {
            at: "2026-06-09T13:04:51Z",
            kind: "hypothesis.formed",
            detail: "cagg 0.99 · suff 7.38",
          },
          {
            at: "2026-06-09T13:05:29Z",
            kind: "hypothesis.routed",
            detail: "admin · ops reviewer",
          },
          {
            at: "2026-06-16T12:48:35Z",
            kind: "decision.created",
            detail: "escalate",
          },
        ]}
        renderDetail={(e) =>
          e.detail ? (
            <span className="text-[11px] text-muted-foreground">
              {String(e.detail)}
            </span>
          ) : null
        }
      />
    </div>
  )
}
```

## Props

### `TimelineIcon`

```ts
export type TimelineIcon = ComponentType<{ className?: string }>
```

### `LifecycleEventStyle`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `icon` | `TimelineIcon` | yes | - | Marker icon. |
| `label` | `string` | yes | - | Human label for the event kind. |
| `iconTone` | `string \| undefined` | no | - | Tailwind text-* class for the icon color. |
| `chipTone` | `string \| undefined` | no | - | Tailwind classes for the marker chip background/text. |

### `LifecycleEvent`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `at` | `string` | yes | - | ISO timestamp used for ordering and the `T+…` offset. |
| `kind` | `string` | yes | - | Event kind, used to resolve style + render the detail body. |
| `detail` | `TDetail \| undefined` | no | - | Arbitrary payload passed to `renderDetail`. |

### `LifecycleTimelineProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `events` | `readonly LifecycleEvent<TDetail>[]` | yes | - | Events in chronological order (first event anchors `T+0`). |
| `eventStyles` | `Record<string, LifecycleEventStyle> \| undefined` | no | - | Style map keyed by `event.kind`. Unknown kinds fall back gracefully. |
| `fallbackStyle` | `LifecycleEventStyle \| undefined` | no | - | Fallback style for kinds missing from `eventStyles`. |
| `renderDetail` | `((event: LifecycleEvent<TDetail>) => ReactNode) \| undefined` | no | - | Renders the per-event detail body. |
| `formatTimestamp` | `((at: string) => ReactNode) \| undefined` | no | - | Formats the absolute timestamp shown on the right. Defaults to ISO. |
| `formatOffset` | `((seconds: number) => ReactNode) \| undefined` | no | - | Formats the elapsed-seconds offset into the `T+…` suffix. |
| `loading` | `boolean \| undefined` | no | `false` | Renders a skeleton instead of events. |
| `emptyLabel` | `ReactNode` | no | - | Message shown when there are no events. |
| `className` | `string \| undefined` | no | - |  |
| `data-testid` | `string \| undefined` | no | - |  |

## Changelog

# lifecycle-timeline

## 0.1.1

- Format source with the repo prettier config (no behavior change).

## 0.1.0

- Initial release, genericized from the `attention-routing-01` block's
  `RoutingFlowTimeline`. Hardcoded event kinds and detail renderers are
  replaced by a caller-supplied `eventStyles` map and a `renderDetail` render
  prop, so any event stream can drive the timeline. Ships empty/loading states
  and a matching `LifecycleTimelineSkeleton`.
