{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "timeline",
  "title": "Timeline",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/timeline/timeline.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport {\n  TimelineContext,\n  TimelineItemContext,\n  type TimelineAlign,\n  type TimelineDensity,\n  type TimelineDirection,\n  type TimelineItemContextValue,\n  type TimelineSide,\n  type TimelineStatus,\n} from './context'\nimport { timelineMediaVariants, type TimelineMediaVariant } from './timeline.variants'\n\n// React's own useId -- SSR-safe and avoids a module-level counter that would\n// hydrate-mismatch. (React 19's useRef requires an initial arg.)\nconst useId = () => React.useId()\n\n/* ------------------------------------------------------------------ Timeline */\n\nexport interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {\n  direction?: TimelineDirection\n  align?: TimelineAlign\n  side?: TimelineSide\n  density?: TimelineDensity\n}\n\nfunction Timeline({\n  className,\n  direction = 'vertical',\n  align = 'start',\n  side,\n  density = 'default',\n  children,\n  ...props\n}: TimelineProps) {\n  const [ids, setIds] = React.useState<string[]>([])\n\n  const register = React.useCallback((id: string) => {\n    setIds((prev) => (prev.includes(id) ? prev : [...prev, id]))\n  }, [])\n  const unregister = React.useCallback((id: string) => {\n    setIds((prev) => prev.filter((i) => i !== id))\n  }, [])\n  const indexOf = React.useCallback((id: string) => ids.indexOf(id), [ids])\n\n  const resolvedSide: TimelineSide = side ?? (direction === 'horizontal' ? 'top' : 'left')\n\n  const ctx = React.useMemo(\n    () => ({\n      direction,\n      align,\n      side: resolvedSide,\n      density,\n      count: ids.length,\n      register,\n      unregister,\n      indexOf,\n    }),\n    [direction, align, resolvedSide, density, ids.length, register, unregister, indexOf],\n  )\n\n  return (\n    <TimelineContext.Provider value={ctx}>\n      <div\n        data-uipkge=\"\"\n        data-slot=\"timeline\"\n        data-direction={direction}\n        data-align={align}\n        className={cn('relative', direction === 'vertical' ? 'flex flex-col' : 'flex flex-row', className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </TimelineContext.Provider>\n  )\n}\n\n/* -------------------------------------------------------------- TimelineItem */\n\nexport interface TimelineItemRenderProps {\n  index: number\n  isLast: boolean\n  side: TimelineSide\n  status: TimelineStatus\n}\n\nexport interface TimelineItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  side?: TimelineSide\n  status?: TimelineStatus\n  children?: React.ReactNode | ((props: TimelineItemRenderProps) => React.ReactNode)\n}\n\nfunction TimelineItem({ className, side, status = 'default', children, ...props }: TimelineItemProps) {\n  const ctx = React.useContext(TimelineContext)\n  const id = useId()\n\n  React.useEffect(() => {\n    if (!ctx) return\n    ctx.register(id)\n    return () => ctx.unregister(id)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [id, ctx?.register, ctx?.unregister])\n\n  const index = ctx ? ctx.indexOf(id) : 0\n  const isFirst = index === 0\n  const isLast = ctx ? index === ctx.count - 1 : false\n\n  const effectiveSide: TimelineSide = (() => {\n    if (side) return side\n    if (!ctx) return 'left'\n    if (ctx.align === 'center') {\n      if (ctx.direction === 'vertical') return index % 2 === 0 ? 'left' : 'right'\n      return index % 2 === 0 ? 'top' : 'bottom'\n    }\n    return ctx.side\n  })()\n\n  const direction = ctx?.direction ?? 'vertical'\n  const density = ctx?.density ?? 'default'\n  const isCenter = ctx?.align === 'center'\n\n  const verticalSpacing = isLast\n    ? ''\n    : {\n        compact: '[&>[data-slot=timeline-content]]:pb-2',\n        default: '[&>[data-slot=timeline-content]]:pb-6',\n        comfortable: '[&>[data-slot=timeline-content]]:pb-10',\n      }[density]\n\n  const horizontalSpacing = isLast\n    ? ''\n    : {\n        compact: '[&>[data-slot=timeline-content]]:pr-3',\n        default: '[&>[data-slot=timeline-content]]:pr-6',\n        comfortable: '[&>[data-slot=timeline-content]]:pr-10',\n      }[density]\n\n  const itemCtx: TimelineItemContextValue = {\n    index,\n    isFirst,\n    isLast,\n    side: effectiveSide,\n    status,\n    direction,\n    density,\n  }\n\n  const resolvedChildren =\n    typeof children === 'function'\n      ? children({ index, isLast, side: effectiveSide, status })\n      : children\n\n  return (\n    <TimelineItemContext.Provider value={itemCtx}>\n      <div\n        data-uipkge=\"\"\n        data-slot=\"timeline-item\"\n        data-side={effectiveSide}\n        data-last={isLast || undefined}\n        className={cn(\n          'relative',\n          // start mode (default): simple flex\n          // No item padding — TimelineMedia's continuous line relies on items\n          // butting up edge-to-edge. Use TimelineContent's own padding for\n          // breathing room between rows/cards.\n          !isCenter &&\n            direction === 'vertical' &&\n            cn('flex gap-4', effectiveSide === 'right' && 'flex-row-reverse text-right', verticalSpacing),\n          !isCenter &&\n            direction === 'horizontal' &&\n            cn(\n              'flex flex-col gap-2',\n              effectiveSide === 'bottom' && 'flex-col-reverse',\n              horizontalSpacing,\n            ),\n          // center alternating: 3-col / 3-row grid\n          isCenter &&\n            direction === 'vertical' &&\n            cn(\n              'grid grid-cols-[1fr_auto_1fr] items-start gap-x-4',\n              '[&>[data-slot=timeline-media]]:col-start-2',\n              '[&>[data-slot=timeline-separator]]:col-start-2',\n              effectiveSide === 'left' &&\n                '[&>[data-slot=timeline-content]]:col-start-1 [&>[data-slot=timeline-content]]:text-right',\n              effectiveSide === 'right' && '[&>[data-slot=timeline-content]]:col-start-3',\n              verticalSpacing,\n            ),\n          isCenter &&\n            direction === 'horizontal' &&\n            cn(\n              'grid grid-rows-[1fr_auto_1fr] items-start gap-y-2',\n              '[&>[data-slot=timeline-media]]:row-start-2',\n              '[&>[data-slot=timeline-separator]]:row-start-2',\n              effectiveSide === 'top' &&\n                '[&>[data-slot=timeline-content]]:row-start-1 [&>[data-slot=timeline-content]]:self-end',\n              effectiveSide === 'bottom' && '[&>[data-slot=timeline-content]]:row-start-3',\n              horizontalSpacing,\n            ),\n          className,\n        )}\n        {...props}\n      >\n        {resolvedChildren}\n      </div>\n    </TimelineItemContext.Provider>\n  )\n}\n\n/* ------------------------------------------------------------- TimelineMedia */\n\nexport interface TimelineMediaProps extends React.HTMLAttributes<HTMLDivElement> {\n  variant?: TimelineMediaVariant\n  status?: TimelineStatus\n  /** Manually hide the auto-generated connector line. */\n  hideConnector?: boolean\n  /**\n   * Color the connector line below the marker using the item's status\n   * (success → green, muted → gray, etc) instead of the neutral border.\n   * Opt-in so existing timelines stay visually unchanged.\n   */\n  coloredConnector?: boolean\n}\n\nfunction TimelineMedia({\n  className,\n  variant = 'dot',\n  status,\n  hideConnector,\n  coloredConnector,\n  children,\n  ...props\n}: TimelineMediaProps) {\n  const item = React.useContext(TimelineItemContext)\n\n  const direction = item?.direction ?? 'vertical'\n  const isFirst = item?.isFirst ?? true\n  const isLast = item?.isLast ?? true\n  const effectiveStatus: TimelineStatus = status ?? item?.status ?? 'default'\n  const showConnector = !hideConnector && !(isFirst && isLast)\n\n  // Marker half-size in rem, used to crop the line so it visually emerges\n  // from the marker center on the first item.\n  const markerHalfRem = {\n    dot: '0.375rem', // size-3 = 12px / 2\n    icon: '1rem', // size-8 = 32px / 2\n    avatar: '1.125rem', // size-9 = 36px / 2\n  }[(variant ?? 'dot') as 'dot' | 'icon' | 'avatar']\n\n  const connectorBgClass = !coloredConnector\n    ? 'bg-border'\n    : {\n        default: 'bg-primary',\n        success: 'bg-success',\n        warning: 'bg-warning',\n        error: 'bg-destructive',\n        info: 'bg-info',\n        muted: 'bg-muted-foreground/40',\n      }[effectiveStatus]\n\n  return (\n    <div\n      data-uipkge=\"\"\n      data-slot=\"timeline-media\"\n      className={cn(\n        'relative shrink-0 self-stretch',\n        direction === 'vertical' ? 'flex w-9 flex-col items-center' : 'flex h-9 flex-row items-center',\n        className,\n      )}\n      style={{ '--timeline-marker-half': markerHalfRem } as React.CSSProperties}\n      {...props}\n    >\n      {/* Single continuous connector line, positioned through the marker.\n          The marker's bg + ring-background acts as a \"punch-through\" so the\n          line appears to break at each marker without any per-item math. */}\n      {showConnector && (\n        <div\n          data-uipkge=\"\"\n          data-slot=\"timeline-media-connector\"\n          aria-hidden=\"true\"\n          className={\n            direction === 'vertical'\n              ? cn('absolute left-1/2 w-px -translate-x-1/2', connectorBgClass)\n              : cn('absolute top-1/2 h-px -translate-y-1/2', connectorBgClass)\n          }\n          style={\n            direction === 'vertical'\n              ? {\n                  top: isFirst ? 'var(--timeline-marker-half)' : '0',\n                  bottom: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n                }\n              : {\n                  left: isFirst ? 'var(--timeline-marker-half)' : '0',\n                  right: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n                }\n          }\n        />\n      )}\n\n      {/* Marker — its bg + ring-background hides the line behind it. */}\n      <div\n        data-uipkge=\"\"\n        data-slot=\"timeline-media-marker\"\n        className={cn(timelineMediaVariants({ variant, status: effectiveStatus }), 'relative z-10')}\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n\n/* --------------------------------------------------------- TimelineSeparator */\n\nexport interface TimelineSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Manually hide the auto-generated connector line. */\n  hideConnector?: boolean\n  /** Override the inner dot (React equivalent of the Vue `#dot` slot). */\n  dot?: React.ReactNode\n}\n\nfunction TimelineSeparator({ className, hideConnector, dot, ...props }: TimelineSeparatorProps) {\n  const item = React.useContext(TimelineItemContext)\n  const direction = item?.direction ?? 'vertical'\n  const isFirst = item?.isFirst ?? true\n  const isLast = item?.isLast ?? true\n  const showConnector = !hideConnector && !(isFirst && isLast)\n\n  return (\n    // Legacy compact separator: 16px marker, customizable inner dot via `dot`\n    // prop. Uses the same single-absolute-line strategy as TimelineMedia so\n    // the connector is pixel-aligned across items. --marker-half = 0.5rem\n    // (= size-4 / 2).\n    <div\n      data-uipkge=\"\"\n      data-slot=\"timeline-separator\"\n      className={cn(\n        'relative shrink-0 self-stretch',\n        direction === 'vertical' ? 'flex w-4 flex-col items-center' : 'flex h-4 flex-row items-center',\n        className,\n      )}\n      style={{ '--timeline-marker-half': '0.5rem' } as React.CSSProperties}\n      {...props}\n    >\n      {showConnector && (\n        <div\n          aria-hidden=\"true\"\n          className={\n            direction === 'vertical'\n              ? 'bg-border absolute left-1/2 w-px -translate-x-1/2'\n              : 'bg-border absolute top-1/2 h-px -translate-y-1/2'\n          }\n          style={\n            direction === 'vertical'\n              ? {\n                  top: isFirst ? 'var(--timeline-marker-half)' : '0',\n                  bottom: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n                }\n              : {\n                  left: isFirst ? 'var(--timeline-marker-half)' : '0',\n                  right: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n                }\n          }\n        />\n      )}\n      <div className=\"bg-primary ring-background relative z-10 flex size-4 items-center justify-center rounded-full ring-4\">\n        {dot ?? <div className=\"bg-primary-foreground size-2 rounded-full\" />}\n      </div>\n    </div>\n  )\n}\n\n/* ----------------------------------------------------------- TimelineContent */\n\nfunction TimelineContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div data-uipkge=\"\" data-slot=\"timeline-content\" className={cn('flex-1 space-y-1', className)} {...props} />\n  )\n}\n\n/* ------------------------------------------------------------- TimelineTitle */\n\nexport interface TimelineTitleProps extends React.HTMLAttributes<HTMLElement> {\n  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div'\n}\n\nfunction TimelineTitle({ as, className, ...props }: TimelineTitleProps) {\n  const Comp = (as ?? 'h3') as React.ElementType\n  return (\n    <Comp\n      data-uipkge=\"\"\n      data-slot=\"timeline-title\"\n      className={cn('text-sm leading-none font-semibold tracking-tight', className)}\n      {...props}\n    />\n  )\n}\n\n/* ------------------------------------------------------- TimelineDescription */\n\nfunction TimelineDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {\n  return (\n    <p\n      data-uipkge=\"\"\n      data-slot=\"timeline-description\"\n      className={cn('text-muted-foreground text-sm', className)}\n      {...props}\n    />\n  )\n}\n\n/* -------------------------------------------------------------- TimelineDate */\n\nfunction TimelineDate({ className, ...props }: React.TimeHTMLAttributes<HTMLTimeElement>) {\n  return (\n    <time\n      data-uipkge=\"\"\n      data-slot=\"timeline-date\"\n      className={cn('text-muted-foreground text-xs', className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Timeline,\n  TimelineItem,\n  TimelineMedia,\n  TimelineSeparator,\n  TimelineContent,\n  TimelineTitle,\n  TimelineDescription,\n  TimelineDate,\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/timeline/timeline.tsx"
    },
    {
      "path": "packages/registry-react/components/timeline/context.ts",
      "content": "import * as React from 'react'\n\nexport type TimelineDirection = 'vertical' | 'horizontal'\nexport type TimelineAlign = 'start' | 'center'\nexport type TimelineSide = 'left' | 'right' | 'top' | 'bottom'\nexport type TimelineStatus = 'default' | 'success' | 'warning' | 'error' | 'info' | 'muted'\nexport type TimelineDensity = 'compact' | 'default' | 'comfortable'\n\nexport interface TimelineContextValue {\n  direction: TimelineDirection\n  align: TimelineAlign\n  side: TimelineSide\n  density: TimelineDensity\n  count: number\n  register: (id: string) => void\n  unregister: (id: string) => void\n  indexOf: (id: string) => number\n}\n\nexport const TimelineContext = React.createContext<TimelineContextValue | null>(null)\n\nexport interface TimelineItemContextValue {\n  index: number\n  isFirst: boolean\n  isLast: boolean\n  side: TimelineSide\n  status: TimelineStatus\n  direction: TimelineDirection\n  density: TimelineDensity\n}\n\nexport const TimelineItemContext = React.createContext<TimelineItemContextValue | null>(null)\n",
      "type": "registry:ui",
      "target": "~/components/ui/timeline/context.ts"
    },
    {
      "path": "packages/registry-react/components/timeline/timeline.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n */\n\nexport const timelineMediaVariants = cva(\n  'relative z-10 flex shrink-0 items-center justify-center rounded-full ring-4 ring-background [&>svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        dot: 'size-3',\n        icon: 'size-8 [&>svg]:size-4',\n        avatar: 'size-9 ring-2 [&>img]:size-full [&>img]:rounded-full [&>img]:object-cover',\n      },\n      status: {\n        default: 'bg-primary text-primary-foreground',\n        success: 'bg-success text-success-foreground',\n        warning: 'bg-warning text-warning-foreground',\n        error: 'bg-destructive text-destructive-foreground',\n        info: 'bg-info text-info-foreground',\n        muted: 'bg-muted text-muted-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'dot',\n      status: 'default',\n    },\n  },\n)\n\nexport type TimelineMediaVariantsProps = VariantProps<typeof timelineMediaVariants>\nexport type TimelineMediaVariant = 'dot' | 'icon' | 'avatar'\n",
      "type": "registry:ui",
      "target": "~/components/ui/timeline/timeline.variants.ts"
    },
    {
      "path": "packages/registry-react/components/timeline/index.ts",
      "content": "export {\n  Timeline,\n  TimelineItem,\n  TimelineMedia,\n  TimelineSeparator,\n  TimelineContent,\n  TimelineTitle,\n  TimelineDescription,\n  TimelineDate,\n  type TimelineProps,\n  type TimelineItemProps,\n  type TimelineItemRenderProps,\n  type TimelineMediaProps,\n  type TimelineSeparatorProps,\n  type TimelineTitleProps,\n} from './timeline'\n\nexport type {\n  TimelineDirection,\n  TimelineAlign,\n  TimelineSide,\n  TimelineStatus,\n  TimelineDensity,\n} from './context'\n\n// Re-export variant API from the sibling file (kept separate to avoid the\n// component <-> index circular import that broke dev SSR for Card).\nexport { timelineMediaVariants, type TimelineMediaVariantsProps, type TimelineMediaVariant } from './timeline.variants'\n",
      "type": "registry:ui",
      "target": "~/components/ui/timeline/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Vertical or horizontal sequence of events with connectors and node markers. Statuses (pending, current, completed, failed) tint the connector. Use for activity feeds, audit logs, and progress tracking.",
  "categories": [
    "data-display"
  ]
}