{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "parallel-timeline",
  "type": "registry:component",
  "title": "Parallel Timeline",
  "description": "Multi-track timeline with shared time axis, BCE/CE event markers, and optional era bands for comparative history.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/parallel-timeline/parallel-timeline.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n  useMemo,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nconst DEFAULT_TICK_COUNT = 8;\n\n/**\n * Color theme for a {@link ParallelTimelineTrack} accent strip and\n * {@link ParallelTimelineEvent} markers.\n *\n * @public\n */\nexport type ParallelTimelineColor =\n  | \"amber\"\n  | \"blue\"\n  | \"emerald\"\n  | \"neutral\"\n  | \"purple\"\n  | \"red\"\n  | \"rose\";\n\nconst COLOR_CLASSES: Record<\n  ParallelTimelineColor,\n  { accent: string; chip: string; marker: string }\n> = {\n  amber: {\n    accent: \"bg-amber-500\",\n    chip: \"bg-amber-500/15 text-amber-700 dark:text-amber-300\",\n    marker: \"border-amber-500 bg-amber-500\",\n  },\n  blue: {\n    accent: \"bg-blue-500\",\n    chip: \"bg-blue-500/15 text-blue-700 dark:text-blue-300\",\n    marker: \"border-blue-500 bg-blue-500\",\n  },\n  emerald: {\n    accent: \"bg-emerald-500\",\n    chip: \"bg-emerald-500/15 text-emerald-700 dark:text-emerald-300\",\n    marker: \"border-emerald-500 bg-emerald-500\",\n  },\n  neutral: {\n    accent: \"bg-muted-foreground/40\",\n    chip: \"bg-muted text-muted-foreground\",\n    marker: \"border-muted-foreground bg-muted-foreground\",\n  },\n  purple: {\n    accent: \"bg-purple-500\",\n    chip: \"bg-purple-500/15 text-purple-700 dark:text-purple-300\",\n    marker: \"border-purple-500 bg-purple-500\",\n  },\n  red: {\n    accent: \"bg-red-500\",\n    chip: \"bg-red-500/15 text-red-700 dark:text-red-300\",\n    marker: \"border-red-500 bg-red-500\",\n  },\n  rose: {\n    accent: \"bg-rose-500\",\n    chip: \"bg-rose-500/15 text-rose-700 dark:text-rose-300\",\n    marker: \"border-rose-500 bg-rose-500\",\n  },\n};\n\n/**\n * One event marker on a {@link ParallelTimelineTrack}.\n *\n * @public\n */\nexport type ParallelTimelineEvent = {\n  /** Stable identifier within the track. */\n  id: string;\n  /** Optional secondary line (date detail, place, etc.). */\n  meta?: ReactNode;\n  /** Event title. */\n  title: ReactNode;\n  /** Year. Negative for BCE / positive for CE. */\n  year: number;\n};\n\n/**\n * Single horizontal track inside a {@link ParallelTimeline}.\n *\n * @public\n */\nexport type ParallelTimelineTrack = {\n  /** Color theme. Defaults to `\"neutral\"`. */\n  color?: ParallelTimelineColor;\n  /** Event markers. */\n  events: ParallelTimelineEvent[];\n  /** Stable identifier. */\n  id: string;\n  /** Display name. */\n  name: ReactNode;\n  /** Optional region label rendered next to the name. */\n  region?: ReactNode;\n};\n\n/**\n * Background era band rendered behind every track.\n *\n * @public\n */\nexport type ParallelTimelineEra = {\n  /** Color theme — drives the band tint. Defaults to `\"neutral\"`. */\n  color?: ParallelTimelineColor;\n  /** End year (inclusive). */\n  end: number;\n  /** Stable identifier. */\n  id: string;\n  /** Display name. */\n  name: ReactNode;\n  /** Start year (inclusive). */\n  start: number;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type ParallelTimelineLabels = {\n  /** Aria-label for the timeline region. Defaults to `\"Parallel timeline\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  region: \"Parallel timeline\",\n} as const satisfies Required<ParallelTimelineLabels>;\n\n/**\n * Props for {@link ParallelTimeline}.\n *\n * @public\n */\nexport type ParallelTimelineProps = {\n  /** End year (positive for CE). */\n  endYear: number;\n  /** Optional background era bands shared across tracks. */\n  eras?: ParallelTimelineEra[];\n  /** Localizable strings. */\n  labels?: ParallelTimelineLabels;\n  /** Start year (negative for BCE). */\n  startYear: number;\n  /** Number of axis ticks to render. Defaults to `8`. */\n  tickCount?: number;\n  /** Track list, rendered in order. */\n  tracks: ParallelTimelineTrack[];\n} & ComponentPropsWithoutRef<\"section\">;\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction formatYear(year: number): string {\n  if (year < 0) return `${Math.abs(year).toString()} BCE`;\n  return `${year.toString()} CE`;\n}\n\nfunction yearToPercent(year: number, start: number, end: number): number {\n  const span = end - start;\n  if (span <= 0) return 0;\n  return clamp(((year - start) / span) * 100, 0, 100);\n}\n\nfunction buildTicks(\n  start: number,\n  end: number,\n  count: number,\n): { label: string; offset: number }[] {\n  const safeCount = Math.max(2, count);\n  const span = end - start;\n  if (span <= 0) return [];\n  const step = span / (safeCount - 1);\n  return Array.from({ length: safeCount }).map((_, index) => {\n    const year = Math.round(start + step * index);\n    return {\n      label: formatYear(year),\n      offset: yearToPercent(year, start, end),\n    };\n  });\n}\n\ntype AxisProps = {\n  ticks: { label: string; offset: number }[];\n};\n\nfunction Axis({ ticks }: AxisProps): ReactNode {\n  return (\n    <div\n      aria-hidden=\"true\"\n      className=\"relative h-7 border-b border-border text-[10px] font-medium uppercase tracking-wide text-muted-foreground\"\n    >\n      {ticks.map((tick) => (\n        <span\n          className=\"absolute top-1 -translate-x-1/2\"\n          key={tick.label}\n          style={{ left: `${tick.offset.toString()}%` }}\n        >\n          {tick.label}\n        </span>\n      ))}\n    </div>\n  );\n}\n\ntype EraBandsProps = {\n  endYear: number;\n  eras: ParallelTimelineEra[];\n  startYear: number;\n};\n\nfunction EraBands({ endYear, eras, startYear }: EraBandsProps): ReactNode {\n  if (eras.length === 0) return null;\n  return (\n    <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n      {eras.map((era) => {\n        const left = yearToPercent(era.start, startYear, endYear);\n        const right = yearToPercent(era.end, startYear, endYear);\n        const width = Math.max(0, right - left);\n        if (width <= 0) return null;\n        const palette = COLOR_CLASSES[era.color ?? \"neutral\"];\n        return (\n          <div\n            className={cn(\"absolute inset-y-0\", palette.chip)}\n            data-era-id={era.id}\n            key={era.id}\n            style={{\n              left: `${left.toString()}%`,\n              width: `${width.toString()}%`,\n            }}\n          />\n        );\n      })}\n    </div>\n  );\n}\n\ntype EventMarkerProps = {\n  color: ParallelTimelineColor;\n  endYear: number;\n  event: ParallelTimelineEvent;\n  startYear: number;\n};\n\nfunction EventMarker({\n  color,\n  endYear,\n  event,\n  startYear,\n}: EventMarkerProps): ReactNode {\n  if (event.year < startYear || event.year > endYear) return null;\n  const left = yearToPercent(event.year, startYear, endYear);\n  const palette = COLOR_CLASSES[color];\n  const titleText = typeof event.title === \"string\" ? event.title : \"\";\n  const ariaLabel = titleText\n    ? `${titleText}, ${formatYear(event.year)}`\n    : undefined;\n  return (\n    <div\n      aria-label={ariaLabel}\n      className=\"absolute top-1/2 z-10 -translate-x-1/2 -translate-y-1/2\"\n      data-event-id={event.id}\n      data-event-year={event.year}\n      style={{ left: `${left.toString()}%` }}\n    >\n      <div\n        aria-hidden=\"true\"\n        className={cn(\n          \"size-3 rounded-full border-2 ring-2 ring-background\",\n          palette.marker,\n        )}\n      />\n      <div className=\"absolute left-1/2 top-4 w-40 -translate-x-1/2 text-center\">\n        <p className=\"truncate text-xs font-medium text-foreground\">\n          {event.title}\n        </p>\n        <p className=\"truncate text-[10px] text-muted-foreground\">\n          {formatYear(event.year)}\n          {event.meta ? <span> · {event.meta}</span> : null}\n        </p>\n      </div>\n    </div>\n  );\n}\n\ntype TrackRowProps = {\n  endYear: number;\n  startYear: number;\n  track: ParallelTimelineTrack;\n};\n\nfunction TrackRow({ endYear, startYear, track }: TrackRowProps): ReactNode {\n  const color = track.color ?? \"neutral\";\n  const palette = COLOR_CLASSES[color];\n  return (\n    <div className=\"relative flex items-stretch gap-3 border-t border-border first:border-t-0\">\n      <div className=\"flex w-32 shrink-0 flex-col gap-1 border-r border-border bg-muted/20 p-3\">\n        <span\n          aria-hidden=\"true\"\n          className={cn(\"h-1 w-8 rounded-full\", palette.accent)}\n        />\n        <p className=\"text-sm font-semibold tracking-tight text-foreground\">\n          {track.name}\n        </p>\n        {track.region ? (\n          <p className=\"text-xs text-muted-foreground\">{track.region}</p>\n        ) : null}\n      </div>\n      <div\n        aria-label={typeof track.name === \"string\" ? track.name : undefined}\n        className=\"relative h-16 min-w-0 flex-1\"\n        data-track-id={track.id}\n      >\n        <div className=\"absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-border\" />\n        {track.events.map((event) => (\n          <EventMarker\n            color={color}\n            endYear={endYear}\n            event={event}\n            key={event.id}\n            startYear={startYear}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Multi-track timeline with a shared time axis. Renders each track as a\n * horizontal lane with event markers positioned by year. Negative years\n * render as `BCE`, positive as `CE`. Background era bands span every\n * track when the consumer passes `eras`.\n *\n * Cross-track connectors, synchronized pan/zoom, collapsible tracks,\n * and click-to-compare are intentionally **out of scope** — drive them\n * from consumer code via the data slots.\n *\n * @example\n * ```tsx\n * <ParallelTimeline\n *   startYear={-500}\n *   endYear={500}\n *   eras={[{ id: \"antiquity\", name: \"Antiquity\", start: -500, end: 500, color: \"neutral\" }]}\n *   tracks={[\n *     {\n *       id: \"rome\",\n *       name: \"Rome\",\n *       color: \"red\",\n *       events: [\n *         { id: \"augustus\", year: -27, title: \"Augustus becomes Emperor\" },\n *         { id: \"fall\", year: 476, title: \"Fall of Western Rome\" },\n *       ],\n *     },\n *     {\n *       id: \"china\",\n *       name: \"China\",\n *       color: \"amber\",\n *       events: [\n *         { id: \"qin\", year: -221, title: \"Qin unifies China\" },\n *         { id: \"han-end\", year: 220, title: \"End of Han Dynasty\" },\n *       ],\n *     },\n *   ]}\n * />\n * ```\n *\n * @public\n */\nexport const ParallelTimeline = forwardRef<HTMLElement, ParallelTimelineProps>(\n  (props, ref) => {\n    const {\n      className,\n      endYear,\n      eras = [],\n      labels,\n      startYear,\n      tickCount = DEFAULT_TICK_COUNT,\n      tracks,\n      ...rest\n    } = props;\n    const resolvedLabels = useMemo(\n      () => ({ ...DEFAULT_LABELS, ...labels }),\n      [labels],\n    );\n    const ticks = useMemo(\n      () => buildTicks(startYear, endYear, tickCount),\n      [endYear, startYear, tickCount],\n    );\n\n    return (\n      <section\n        aria-label={resolvedLabels.region}\n        className={cn(\n          \"flex w-full flex-col overflow-x-auto rounded-2xl border bg-background text-foreground\",\n          className,\n        )}\n        ref={ref}\n        {...rest}\n      >\n        <div className=\"flex items-stretch gap-3 border-b border-border\">\n          <div aria-hidden=\"true\" className=\"w-32 shrink-0\" />\n          <Axis ticks={ticks} />\n        </div>\n        <div className=\"relative\">\n          <EraBands endYear={endYear} eras={eras} startYear={startYear} />\n          {tracks.map((track) => (\n            <TrackRow\n              endYear={endYear}\n              key={track.id}\n              startYear={startYear}\n              track={track}\n            />\n          ))}\n        </div>\n      </section>\n    );\n  },\n);\nParallelTimeline.displayName = \"ParallelTimeline\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
