{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "timeline",
  "type": "registry:ui",
  "title": "Timeline",
  "description": "Vertical scroll-driven timeline whose beam fills as the reader progresses. For changelogs and stories.",
  "dependencies": [
    "@saasflare/ui",
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/timeline.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n/**\n * @fileoverview Vertical timeline with scroll-driven beam animation.\n * @author Saasflare™\n * A chronological timeline where a beam progresses as the user scrolls.\n * Ideal for changelogs, roadmaps, and process steps.\n * @module packages/ui/components/ui/timeline\n * @package ui\n *\n * @component\n * @example\n * import { Timeline, TimelineItem } from '@saasflare/ui';\n * <Timeline>\n *   <TimelineItem title=\"v2.0 Released\" date=\"March 2026\">\n *     Major redesign with new dashboard.\n *   </TimelineItem>\n *   <TimelineItem title=\"v1.5 Released\" date=\"January 2026\">\n *     Added billing module.\n *   </TimelineItem>\n * </Timeline>\n */\n\nimport { useRef, type ReactNode } from \"react\"\nimport { m, useScroll, useTransform } from \"motion/react\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { useSaasflareMotion, springGentle } from \"@saasflare/ui\"\n\n/** Props for the Timeline container. */\nexport interface TimelineProps extends SaasflareComponentProps {\n  /** TimelineItem children. */\n  children: ReactNode\n  /** Additional class names. */\n  className?: string\n}\n\n/**\n * Vertical timeline with a scroll-driven progress beam.\n *\n * - Beam fills as the user scrolls through the timeline\n * - Each item fades in on scroll intersection\n * - Falls back to a static timeline when motion is disabled (reduced-motion or `animated={false}`)\n *\n * @component\n * @package ui\n */\nexport function Timeline({\n  children,\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n}: TimelineProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const motion = useSaasflareMotion(sf.animated, springGentle)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const { scrollYProgress } = useScroll({\n    target: containerRef,\n    offset: [\"start 80%\", \"end 50%\"],\n  })\n\n  // Hook order stays stable; the beam render is gated on `motion.disabled`\n  // below so the scroll-driven progress honors the `animated` axis.\n  const beamHeight = useTransform(scrollYProgress, [0, 1], [\"0%\", \"100%\"])\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative\", className)}\n      data-slot=\"timeline\"\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n    >\n      {/* Track line */}\n      <div className=\"absolute left-4 top-0 h-full w-0.5 bg-border md:left-1/2 md:-translate-x-px\" />\n\n      {/* Animated beam overlay */}\n      {!motion.disabled && (\n        <m.div\n          className=\"absolute left-4 top-0 w-0.5 bg-primary md:left-1/2 md:-translate-x-px\"\n          style={{ height: beamHeight }}\n        />\n      )}\n\n      {/* Items */}\n      <div className=\"relative space-y-12\">{children}</div>\n    </div>\n  )\n}\n\n/** Props for a TimelineItem. */\nexport interface TimelineItemProps extends SaasflareComponentProps {\n  /** Item title. */\n  title: string\n  /** Date or time label. */\n  date?: string\n  /** Item content/description. */\n  children: ReactNode\n  /** Additional class names. */\n  className?: string\n}\n\n/**\n * Individual item within a Timeline.\n *\n * - Content sits to the right of the track on desktop, left-aligned on mobile\n * - Dot indicator on the timeline track\n * - Fades in when scrolled into view (disabled when motion is off)\n *\n * @component\n * @package ui\n */\nexport function TimelineItem({\n  title,\n  date,\n  children,\n  className,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n}: TimelineItemProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const motion = useSaasflareMotion(sf.animated, springGentle)\n\n  const inner = (\n    <>\n      {/* Dot */}\n      <div className=\"absolute left-[11px] top-1.5 size-3 rounded-full border-2 border-primary bg-background md:left-1/2 md:-translate-x-1.5\" />\n\n      {/* Content card */}\n      <div className=\"md:ml-[calc(50%+2rem)] md:max-w-[calc(50%-3rem)]\">\n        {date && (\n          <span className=\"mb-1 block text-sm text-muted-foreground\">{date}</span>\n        )}\n        <h3 className=\"text-lg font-semibold\">{title}</h3>\n        <div className=\"mt-1 text-muted-foreground\">{children}</div>\n      </div>\n    </>\n  )\n\n  // Motion off: render a plain <div> so consumers don't instantiate a motion\n  // node per item — and don't need the LazyMotion provider just for a Timeline.\n  if (motion.disabled) {\n    return (\n      <div\n        className={cn(\"relative pl-12 md:pl-0\", className)}\n        data-slot=\"timeline-item\"\n        data-surface={sf.surface}\n        data-radius={sf.radius}\n        data-animated={String(sf.animated)}\n      >\n        {inner}\n      </div>\n    )\n  }\n\n  return (\n    <m.div\n      initial={{ opacity: 0, y: 16 }}\n      whileInView={{ opacity: 1, y: 0 }}\n      viewport={{ once: true, margin: \"-80px\" }}\n      transition={motion.transition}\n      className={cn(\"relative pl-12 md:pl-0\", className)}\n      data-slot=\"timeline-item\"\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n    >\n      {inner}\n    </m.div>\n  )\n}\n",
      "target": "components/ui/timeline.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/timeline.json"
  }
}
