{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "demo-scroll-chapters",
  "title": "Scroll Chapters Demo",
  "description": "Five chapters with a draggable, chaptered progress-bar nav — two of them opt out of showing a panel.",
  "registryDependencies": ["@soralabs/scroll-chapters"],
  "files": [
    {
      "path": "registry/demo/primitives/effects/scroll-chapters/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { CatalogScrollHint } from \"@/components/catalog/catalog-scroll-hint\";\nimport { resolveScrollRoot } from \"@/lib/catalog/resolve-scroll-root\";\nimport { waitForScrollerReady } from \"@/lib/scroll/scroller-ready\";\nimport {\n  ScrollChapters,\n  type ScrollChaptersChapter,\n} from \"@/components/sora-ui/effects/scroll-chapters\";\n\n// Same copy, colors, and per-chapter panel opt-in/opt-out as the reference\n// implementation at apps/www/app/demo/gsap-scrollbar-demo.tsx (kept\n// untouched, hand-rolled) — this is that same design, wired through the\n// generalized `ScrollChapters` primitive instead.\n\nfunction ChapterBody({\n  body,\n  heading,\n  index,\n}: {\n  body: string;\n  heading: string;\n  index: number;\n}) {\n  return (\n    <div className=\"flex h-full flex-col justify-center gap-4 border-current/10 border-t px-6 py-16 first:border-t-0 md:px-16\">\n      <span className=\"font-mono text-[#ff4433] text-sm tracking-wide\">\n        {String(index + 1).padStart(2, \"0\")}\n      </span>\n      {/* No color class here — inherits from whatever `className` the\n          consumer passes to `<ScrollChapters>` on the root section. */}\n      <h3 className=\"m-0 font-semibold text-[clamp(2rem,5vw,3.5rem)] leading-[1.05] tracking-[-0.02em]\">\n        {heading}\n      </h3>\n      <p className=\"m-0 max-w-[42rem] text-[1.05rem] leading-[1.6] opacity-75\">\n        {body}\n      </p>\n    </div>\n  );\n}\n\nfunction CodePreview({ code }: { code: string }) {\n  return (\n    <pre className=\"m-0 overflow-x-auto whitespace-pre-wrap leading-[1.3]\">\n      {/* Inherits from the panel wrapper's own `text-[#f6f4f2]` (see\n          `classNames.panel` below) — the panel's background is always\n          dark regardless of the section's `className`, so its text\n          color is pinned there instead of following the section. */}\n      <code className=\"font-mono text-[0.75rem]\">{code}</code>\n    </pre>\n  );\n}\n\n// Not every panel has to be a code snippet — same as the original\n// anime.js reference, which mixes `<pre><code>` panels with a bundle-size\n// bar chart and a sponsor badge. A row of ticks was tried here first, but\n// sitting right above the track's own tick-marked ruler it just read as\n// a second, redundant scrollbar — a plain stat badge (matching the\n// original's sponsor/funding-level pill) reads cleaner.\nfunction StatRow({ label, value }: { label: string; value: string }) {\n  return (\n    <div className=\"flex items-center gap-3\">\n      <span className=\"h-2 w-2 shrink-0 rounded-full bg-[#ff4433]\" />\n      <span className=\"font-mono text-xs uppercase tracking-wide opacity-60\">\n        {label}\n      </span>\n      <span className=\"font-mono text-sm\">{value}</span>\n    </div>\n  );\n}\n\n// Same taller shape as `GhostComparisonPreview` below — a raw-to-snapped\n// value pair reads more concretely than a single \"1 / 65\" label, and\n// keeps every panel from settling at the same one-line height.\nfunction SnapStepComparisonPreview() {\n  return (\n    <div className=\"flex flex-col gap-2.5\">\n      <StatRow label=\"Raw progress\" value=\"0.4523\" />\n      <StatRow label=\"Snapped to\" value=\"0.4615 (30 / 65)\" />\n      <div className=\"h-px bg-current opacity-10\" />\n      <p className=\"m-0 text-xs leading-snug opacity-70\">\n        Every drag/click position rounds to the nearest 1/65th step before it\n        ever reaches the scroll position.\n      </p>\n    </div>\n  );\n}\n\n// A taller panel — a couple of stat rows plus a caption, instead of a\n// single line, the same way the original's own panels vary in height\n// (a five-line code block reads very differently from a one-line badge).\nfunction GhostComparisonPreview() {\n  return (\n    <div className=\"flex flex-col gap-2.5\">\n      <StatRow label=\"Real cursor\" value=\"pointer-events: auto\" />\n      <StatRow label=\"Ghost cursor\" value=\"opacity: 0.45\" />\n      <div className=\"h-px bg-current opacity-10\" />\n      <p className=\"m-0 text-xs leading-snug opacity-70\">\n        Only the real cursor is ever draggable — the ghost just previews where a\n        click on the track would seek to.\n      </p>\n    </div>\n  );\n}\n\nconst CHAPTERS: ScrollChaptersChapter[] = [\n  {\n    content: (\n      <ChapterBody\n        body=\"The pill on the track in the corner is a real gsap.Draggable instance, bounded to the track element. Grab it and drag — the page scrolls with you. (No panel here — the scrollbar itself is still hidden this close to the top.)\"\n        heading=\"Grab the cursor\"\n        index={0}\n      />\n    ),\n    id: \"drag\",\n  },\n  {\n    content: (\n      <ChapterBody\n        body=\"Scrolling this section moves the cursor. Dragging the cursor scrolls this section. Both directions read from and write to the same ScrollTrigger instance.\"\n        heading=\"Two-way sync\"\n        index={1}\n      />\n    ),\n    id: \"sync\",\n    preview: (\n      <CodePreview\n        code={\n          \"const st = ScrollTrigger.create({\\n  trigger: section,\\n  onUpdate: sync,\\n});\\n\\nst.scroll(interpolate(p));\"\n        }\n      />\n    ),\n  },\n  {\n    content: (\n      <ChapterBody\n        body=\"Clicking or dragging the track doesn't land on an arbitrary pixel — the position snaps to a fixed step, so seeking feels deliberate instead of jittery.\"\n        heading=\"Snap to step\"\n        index={2}\n      />\n    ),\n    id: \"snap\",\n    preview: <SnapStepComparisonPreview />,\n  },\n  {\n    content: (\n      <ChapterBody\n        body=\"Hover the track without pressing anything and a second, translucent cursor previews where a click would seek to — it disappears the moment you actually grab the real one.\"\n        heading=\"Ghost preview\"\n        index={3}\n      />\n    ),\n    id: \"ghost\",\n    preview: <GhostComparisonPreview />,\n  },\n  {\n    content: (\n      <ChapterBody\n        body=\"Each section independently decides whether to show a panel, reporting its own active state and panel visibility up to shared React state — nothing here reaches into another section's internals. (No panel on this last one either — same reason as the first: the scrollbar is already hidden this close to the bottom.)\"\n        heading=\"Chapter panels\"\n        index={4}\n      />\n    ),\n    id: \"panels\",\n  },\n];\n\nconst TRACK_STYLE_CLASS =\n  \"[background-image:linear-gradient(to_right,rgb(242_242_237/18%)_1px,transparent_1px)] [background-size:1.53846%_0.5rem]\";\n\nexport default function ScrollChaptersExample() {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [scroller, setScroller] = useState<Element | Window | undefined>();\n\n  useEffect(() => {\n    const node = rootRef.current;\n    if (!node) {\n      return;\n    }\n\n    const resolved = resolveScrollRoot(node);\n\n    waitForScrollerReady(resolved)\n      .then(() => {\n        setScroller(resolved);\n      })\n      .catch(() => {\n        /* preview unmounted */\n      });\n  }, []);\n\n  return (\n    <div className=\"w-full\" ref={rootRef}>\n      <CatalogScrollHint label=\"Scroll to drag the cursor and flip chapters\" />\n\n      {scroller ? (\n        <ScrollChapters\n          chapters={CHAPTERS}\n          className=\"bg-secondary text-primary!\"\n          classNames={{\n            cursor: \"bg-[#ff4433]\",\n            cursorGhost: \"bg-[#ff4433]\",\n            panel: \"rounded-xl bg-[#17171a] px-4 py-2.5 text-[#f6f4f2]\",\n            progressCard: \"rounded-xl bg-[#17171a]\",\n            tick: \"border-[#f6f4f2]\",\n            track: TRACK_STYLE_CLASS,\n          }}\n          containerQuery\n          embedded\n          hideCardAtEnd={false}\n          hideCardAtStart={false}\n          scroller={scroller}\n        />\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/sora-ui/demo/effects/scroll-chapters.tsx"
    }
  ],
  "type": "registry:ui"
}
