{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "historic-timeline",
  "type": "registry:component",
  "title": "Historic Timeline",
  "description": "Specialized timeline for historical events spanning centuries or millennia, with era bands, period bars, and BCE/CE point markers.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/historic-timeline/historic-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 eras and event categories.\n *\n * @public\n */\nexport type HistoricColor =\n  | \"amber\"\n  | \"blue\"\n  | \"emerald\"\n  | \"neutral\"\n  | \"purple\"\n  | \"red\"\n  | \"rose\";\n\nconst COLOR_PALETTE: Record<\n  HistoricColor,\n  { band: string; chip: string; marker: string }\n> = {\n  amber: {\n    band: \"bg-amber-500/15\",\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    band: \"bg-blue-500/15\",\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    band: \"bg-emerald-500/15\",\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    band: \"bg-muted\",\n    chip: \"bg-muted text-muted-foreground\",\n    marker: \"border-muted-foreground bg-muted-foreground\",\n  },\n  purple: {\n    band: \"bg-purple-500/15\",\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    band: \"bg-red-500/15\",\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    band: \"bg-rose-500/15\",\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 * Background era band rendered behind the timeline.\n *\n * @public\n */\nexport type HistoricEra = {\n  /** Color theme. Defaults to `\"neutral\"`. */\n  color?: HistoricColor;\n  /** End year (inclusive). */\n  endYear: number;\n  /** Stable identifier. */\n  id: string;\n  /** Display name. */\n  name: ReactNode;\n  /** Start year (inclusive). */\n  startYear: number;\n};\n\n/**\n * Mapping from category id to color theme + display label.\n *\n * @public\n */\nexport type HistoricCategory = {\n  /** Color theme. Defaults to `\"neutral\"`. */\n  color?: HistoricColor;\n  /** Stable identifier; matches {@link HistoricEvent.category}. */\n  id: string;\n  /** Display label. */\n  label: ReactNode;\n};\n\n/**\n * Point-in-time event marker.\n *\n * @public\n */\nexport type HistoricEvent = {\n  /** Optional category id. Drives the marker color when provided. */\n  category?: string;\n  /** Optional description. */\n  description?: ReactNode;\n  /** Optional anchor href. Renders the event title as a link. */\n  href?: string;\n  /** Stable identifier. */\n  id: string;\n  /** Event title. */\n  title: ReactNode;\n  /** Year. Negative for BCE / positive for CE. */\n  year: number;\n};\n\n/**\n * Span event (a period or duration).\n *\n * @public\n */\nexport type HistoricPeriod = {\n  /** Optional category id. Drives the band color when provided. */\n  category?: string;\n  /** End year (inclusive). */\n  endYear: number;\n  /** Stable identifier. */\n  id: string;\n  /** Start year (inclusive). */\n  startYear: number;\n  /** Display title. */\n  title: ReactNode;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type HistoricTimelineLabels = {\n  /** Aria-label for the timeline section. Defaults to `\"Historic timeline\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  region: \"Historic timeline\",\n} as const satisfies Required<HistoricTimelineLabels>;\n\n/**\n * Props for {@link HistoricTimeline}.\n *\n * @public\n */\nexport type HistoricTimelineProps = {\n  /** Optional category list — drives event marker colors and the legend. */\n  categories?: HistoricCategory[];\n  /** End year of the visible window. */\n  endYear: number;\n  /** Eras rendered as background bands. */\n  eras?: HistoricEra[];\n  /** Point-in-time event markers. */\n  events?: HistoricEvent[];\n  /** Localizable strings. */\n  labels?: HistoricTimelineLabels;\n  /** Span events (durations). */\n  periods?: HistoricPeriod[];\n  /** Start year of the visible window (negative for BCE). */\n  startYear: number;\n  /** Number of axis ticks. Defaults to `8`. */\n  tickCount?: number;\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\nfunction resolveCategoryColor(\n  categoryId: string | undefined,\n  categories: HistoricCategory[],\n): HistoricColor {\n  if (!categoryId) return \"neutral\";\n  const match = categories.find((category) => category.id === categoryId);\n  return match?.color ?? \"neutral\";\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: HistoricEra[];\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.startYear, startYear, endYear);\n        const right = yearToPercent(era.endYear, startYear, endYear);\n        const width = Math.max(0, right - left);\n        if (width <= 0) return null;\n        const palette = COLOR_PALETTE[era.color ?? \"neutral\"];\n        return (\n          <div\n            className={cn(\"absolute inset-y-0\", palette.band)}\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 EraLabelsProps = {\n  endYear: number;\n  eras: HistoricEra[];\n  startYear: number;\n};\n\nfunction EraLabels({ endYear, eras, startYear }: EraLabelsProps): ReactNode {\n  if (eras.length === 0) return null;\n  return (\n    <div className=\"relative flex h-6 border-b border-border\">\n      {eras.map((era) => {\n        const left = yearToPercent(era.startYear, startYear, endYear);\n        const right = yearToPercent(era.endYear, startYear, endYear);\n        const width = Math.max(0, right - left);\n        if (width <= 0) return null;\n        const palette = COLOR_PALETTE[era.color ?? \"neutral\"];\n        return (\n          <span\n            className={cn(\n              \"absolute top-1 truncate rounded px-1 text-[10px] font-semibold uppercase tracking-wide\",\n              palette.chip,\n            )}\n            key={`${era.id}-label`}\n            style={{\n              left: `${left.toString()}%`,\n              width: `${width.toString()}%`,\n            }}\n          >\n            {era.name}\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\ntype PeriodLaneProps = {\n  categories: HistoricCategory[];\n  endYear: number;\n  periods: HistoricPeriod[];\n  startYear: number;\n};\n\nfunction PeriodLane({\n  categories,\n  endYear,\n  periods,\n  startYear,\n}: PeriodLaneProps): ReactNode {\n  if (periods.length === 0) return null;\n  return (\n    <div className=\"relative flex h-7 items-center border-b border-border/60\">\n      {periods.map((period) => {\n        const left = yearToPercent(period.startYear, startYear, endYear);\n        const right = yearToPercent(period.endYear, startYear, endYear);\n        const width = Math.max(0, right - left);\n        if (width <= 0) return null;\n        const color = resolveCategoryColor(period.category, categories);\n        const palette = COLOR_PALETTE[color];\n        const titleText = typeof period.title === \"string\" ? period.title : \"\";\n        return (\n          <div\n            aria-label={\n              titleText\n                ? `${titleText}, ${formatYear(period.startYear)} – ${formatYear(period.endYear)}`\n                : undefined\n            }\n            className={cn(\n              \"absolute top-1 flex h-5 items-center overflow-hidden rounded-sm px-1 text-[10px] font-medium\",\n              palette.chip,\n            )}\n            data-period-id={period.id}\n            key={period.id}\n            style={{\n              left: `${left.toString()}%`,\n              width: `${width.toString()}%`,\n            }}\n          >\n            <span className=\"truncate\">{period.title}</span>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\ntype EventMarkerProps = {\n  categories: HistoricCategory[];\n  endYear: number;\n  event: HistoricEvent;\n  startYear: number;\n};\n\nfunction EventMarker({\n  categories,\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 color = resolveCategoryColor(event.category, categories);\n  const palette = COLOR_PALETTE[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-44 -translate-x-1/2 text-center\">\n        {event.href ? (\n          <a\n            className=\"block truncate text-xs font-medium text-foreground underline-offset-4 hover:underline\"\n            href={event.href}\n          >\n            {event.title}\n          </a>\n        ) : (\n          <p className=\"truncate text-xs font-medium text-foreground\">\n            {event.title}\n          </p>\n        )}\n        <p className=\"truncate text-[10px] text-muted-foreground\">\n          {formatYear(event.year)}\n          {event.description ? <span> · {event.description}</span> : null}\n        </p>\n      </div>\n    </div>\n  );\n}\n\ntype EventLaneProps = {\n  categories: HistoricCategory[];\n  endYear: number;\n  events: HistoricEvent[];\n  startYear: number;\n};\n\nfunction EventLane({\n  categories,\n  endYear,\n  events,\n  startYear,\n}: EventLaneProps): ReactNode {\n  return (\n    <div className=\"relative h-20\">\n      <div className=\"absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-border\" />\n      {events.map((event) => (\n        <EventMarker\n          categories={categories}\n          endYear={endYear}\n          event={event}\n          key={event.id}\n          startYear={startYear}\n        />\n      ))}\n    </div>\n  );\n}\n\ntype LegendProps = {\n  categories: HistoricCategory[];\n};\n\nfunction Legend({ categories }: LegendProps): ReactNode {\n  if (categories.length === 0) return null;\n  return (\n    <div className=\"flex flex-wrap gap-1.5 border-t border-border px-3 py-2 text-xs\">\n      {categories.map((category) => {\n        const palette = COLOR_PALETTE[category.color ?? \"neutral\"];\n        return (\n          <span\n            className={cn(\n              \"inline-flex items-center gap-1 rounded-full px-2 py-0.5\",\n              palette.chip,\n            )}\n            data-category-id={category.id}\n            key={category.id}\n          >\n            <span\n              aria-hidden=\"true\"\n              className={cn(\"size-2 rounded-full\", palette.marker)}\n            />\n            {category.label}\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\n/**\n * Specialized timeline for historical events spanning centuries or\n * millennia. Renders era bands as a background layer, period bars in a\n * lane, and point events as markers. Negative years format as `BCE`,\n * positive as `CE`. Composes nothing — pure CSS.\n *\n * @example\n * ```tsx\n * <HistoricTimeline\n *   startYear={-500}\n *   endYear={2025}\n *   eras={[{ id: \"ancient\", name: \"Ancient\", startYear: -3000, endYear: 476, color: \"amber\" }]}\n *   periods={[{ id: \"100yr\", title: \"Hundred Years' War\", startYear: 1337, endYear: 1453, category: \"conflict\" }]}\n *   events={[\n *     { id: \"olympics\", year: -776, title: \"First Olympic Games\", category: \"culture\" },\n *     { id: \"moon\", year: 1969, title: \"Moon landing\", category: \"science\" },\n *   ]}\n *   categories={[\n *     { id: \"culture\", label: \"Culture\", color: \"amber\" },\n *     { id: \"science\", label: \"Science\", color: \"blue\" },\n *     { id: \"conflict\", label: \"Conflict\", color: \"red\" },\n *   ]}\n * />\n * ```\n *\n * @public\n */\nexport const HistoricTimeline = forwardRef<HTMLElement, HistoricTimelineProps>(\n  (props, ref) => {\n    const {\n      categories = [],\n      className,\n      endYear,\n      eras = [],\n      events = [],\n      labels,\n      periods = [],\n      startYear,\n      tickCount = DEFAULT_TICK_COUNT,\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-hidden rounded-2xl border bg-background text-foreground\",\n          className,\n        )}\n        ref={ref}\n        {...rest}\n      >\n        <Axis ticks={ticks} />\n        <EraLabels endYear={endYear} eras={eras} startYear={startYear} />\n        <PeriodLane\n          categories={categories}\n          endYear={endYear}\n          periods={periods}\n          startYear={startYear}\n        />\n        <div className=\"relative\">\n          <EraBands endYear={endYear} eras={eras} startYear={startYear} />\n          <EventLane\n            categories={categories}\n            endYear={endYear}\n            events={events}\n            startYear={startYear}\n          />\n        </div>\n        <Legend categories={categories} />\n      </section>\n    );\n  },\n);\nHistoricTimeline.displayName = \"HistoricTimeline\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
