{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thread-timeline",
  "type": "registry:ui",
  "title": "Thread Timeline",
  "description": "A timeline whose thread is sewn down the page as you read, lighting each node as it reaches it.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/thread-timeline.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface ThreadTimelineProps extends React.ComponentProps<\"ol\"> {\n  /** Where the thread head sits in the viewport, 0 at the top, 1 at the bottom. */\n  line?: number\n}\n\nexport function ThreadTimeline({\n  children,\n  className,\n  line = 0.55,\n  style,\n  ...props\n}: ThreadTimelineProps) {\n  const ref = React.useRef<HTMLOListElement>(null)\n\n  React.useEffect(() => {\n    const list = ref.current\n    if (!list) {\n      return\n    }\n\n    const settle = (progress: number, reached: boolean) => {\n      list.style.setProperty(\"--thread-progress\", `${progress}`)\n      for (const item of list.querySelectorAll<HTMLElement>(\n        \"[data-slot='thread-timeline-item']\"\n      )) {\n        item.toggleAttribute(\"data-reached\", reached)\n      }\n    }\n\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      settle(1, true)\n      return\n    }\n\n    let frame = 0\n\n    const measure = () => {\n      const head = window.innerHeight * line\n      const rect = list.getBoundingClientRect()\n      const progress = rect.height > 0 ? (head - rect.top) / rect.height : 1\n      list.style.setProperty(\n        \"--thread-progress\",\n        `${Math.min(Math.max(progress, 0), 1)}`\n      )\n\n      // Each node is measured against the same head the thread is drawn to, so\n      // a node can never light up ahead of the thread that reaches it.\n      for (const item of list.querySelectorAll<HTMLElement>(\n        \"[data-slot='thread-timeline-item']\"\n      )) {\n        const node = item.querySelector<HTMLElement>(\n          \"[data-slot='thread-timeline-node']\"\n        )\n        const box = (node ?? item).getBoundingClientRect()\n        item.toggleAttribute(\"data-reached\", box.top + box.height / 2 <= head)\n      }\n    }\n\n    const onScroll = () => {\n      cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(measure)\n    }\n\n    measure()\n    window.addEventListener(\"scroll\", onScroll, { passive: true })\n    window.addEventListener(\"resize\", onScroll, { passive: true })\n    return () => {\n      window.removeEventListener(\"scroll\", onScroll)\n      window.removeEventListener(\"resize\", onScroll)\n      cancelAnimationFrame(frame)\n    }\n  }, [line])\n\n  return (\n    <ol\n      ref={ref}\n      data-slot=\"thread-timeline\"\n      className={cn(\"relative\", className)}\n      style={{ \"--thread-progress\": \"0\", ...style } as React.CSSProperties}\n      {...props}\n    >\n      {/* The pattern to sew along, and the thread that has reached it. */}\n      <span\n        aria-hidden=\"true\"\n        className=\"border-muted-foreground/25 absolute top-1 bottom-1 left-2 border-l border-dashed\"\n      />\n      <span\n        aria-hidden=\"true\"\n        className=\"bg-primary absolute top-1 bottom-1 left-2 w-px origin-top\"\n        style={{ scale: \"1 var(--thread-progress)\" }}\n      />\n      {children}\n    </ol>\n  )\n}\n\nexport interface ThreadTimelineItemProps extends Omit<\n  React.ComponentProps<\"li\">,\n  \"title\"\n> {\n  /** Heading for the entry. */\n  title?: React.ReactNode\n  /** Small line above the title. A date, usually. */\n  meta?: React.ReactNode\n  /** Content for the node itself. A number or a glyph, if anything. */\n  marker?: React.ReactNode\n}\n\nexport function ThreadTimelineItem({\n  title,\n  meta,\n  marker,\n  children,\n  className,\n  ...props\n}: ThreadTimelineItemProps) {\n  return (\n    <li\n      data-slot=\"thread-timeline-item\"\n      className={cn(\"group relative pb-9 pl-9 last:pb-0\", className)}\n      {...props}\n    >\n      <span\n        data-slot=\"thread-timeline-node\"\n        aria-hidden=\"true\"\n        className=\"bg-background border-muted-foreground/40 text-primary-foreground group-data-[reached]:border-primary group-data-[reached]:bg-primary absolute top-1 left-2 grid size-4 -translate-x-1/2 place-items-center rounded-full border-2 text-[0.5rem] font-medium transition duration-[240ms] ease-[var(--ease-out-quart)] group-data-[reached]:scale-110 motion-reduce:transition-none\"\n      >\n        {marker}\n      </span>\n\n      <div className=\"translate-x-1 opacity-55 transition duration-[240ms] ease-[var(--ease-out-quart)] group-data-[reached]:translate-x-0 group-data-[reached]:opacity-100 motion-reduce:transition-none\">\n        {meta ? (\n          <div className=\"text-muted-foreground text-xs tracking-wide\">\n            {meta}\n          </div>\n        ) : null}\n        {title ? (\n          <h3 className=\"mt-1 text-sm leading-none font-medium\">{title}</h3>\n        ) : null}\n        {children ? (\n          <div className=\"text-muted-foreground mt-2 text-sm\">{children}</div>\n        ) : null}\n      </div>\n    </li>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-out-quart": "cubic-bezier(0.165, 0.84, 0.44, 1)"
    }
  }
}