{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-story",
  "type": "registry:component",
  "title": "Scroll story",
  "description": "A scrollytelling section: steps scroll on one side while a pinned visual swaps as each reaches mid screen and a progress rail fills. Inline on mobile.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/blocks/scroll-story.tsx",
      "type": "registry:ui",
      "target": "components/ui/scroll-story.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { CheckIcon, DatabaseIcon, MessageSquareIcon, ZapIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * ScrollStory: steps on one side, a pinned visual on the other that swaps\n * as each step reaches the middle of the screen. A rail fills with progress.\n * On small screens each step shows its own visual inline.\n */\n\ntype Step = { kicker?: string; title: string; body: string; visual?: React.ReactNode }\n\ntype ScrollStoryProps = {\n  eyebrow?: string\n  title?: React.ReactNode\n  steps?: Step[]\n  className?: string\n}\n\nfunction Panel({ icon: Icon, label, rows }: { icon: typeof ZapIcon; label: string; rows: [string, string][] }) {\n  return (\n    <div className=\"w-full max-w-md rounded-2xl border border-border bg-raised p-5 shadow-overlay\">\n      <div className=\"flex items-center gap-2.5\">\n        <span className=\"grid size-8 place-items-center rounded-lg bg-accent text-on-accent shadow-ink\">\n          <Icon className=\"size-4\" />\n        </span>\n        <p className=\"text-[13.5px] font-medium text-fg\">{label}</p>\n      </div>\n      <ul className=\"mt-4 space-y-2\">\n        {rows.map(([a, b], i) => (\n          <li key={a} className=\"flex animate-[rise-in_400ms_var(--ease-hairline)_both] items-center justify-between rounded-lg bg-sunken px-3 py-2.5 text-[13px]\" style={{ animationDelay: `${i * 90}ms` }}>\n            <span className=\"flex items-center gap-2 text-fg\">\n              <CheckIcon className=\"size-3.5 text-success\" strokeWidth={3} />\n              {a}\n            </span>\n            <span className=\"font-mono text-[11.5px] text-fg-muted\">{b}</span>\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nconst STEPS: Step[] = [\n  { kicker: \"01 · Connect\", title: \"Point it at your tools.\", body: \"Stripe, your database, the CRM. Read-only keys, two clicks each. Nothing to install.\", visual: <Panel icon={DatabaseIcon} label=\"Sources connected\" rows={[[\"Stripe\", \"12,480 events\"], [\"Postgres\", \"38 tables\"], [\"HubSpot\", \"2,104 deals\"]]} /> },\n  { kicker: \"02 · Learn\", title: \"It learns what normal looks like.\", body: \"Lumen builds a baseline for every metric, by weekday and season, so it knows a real spike from a Monday.\", visual: <Panel icon={ZapIcon} label=\"Baselines ready\" rows={[[\"MRR\", \"±2.1% normal\"], [\"Refunds\", \"±6.8% normal\"], [\"Churn\", \"±0.3% normal\"]]} /> },\n  { kicker: \"03 · Tell\", title: \"It tells you when it matters.\", body: \"An alert in Slack with the number, the reason, and a suggested next step. You reply to act.\", visual: <Panel icon={MessageSquareIcon} label=\"#finance · Lumen\" rows={[[\"Refunds +220%\", \"Sun 14:02\"], [\"Cause: batch #4411\", \"found\"], [\"Draft reply sent\", \"you\"]]} /> },\n]\n\nfunction ScrollStory({ eyebrow = \"How it works\", title = \"Live in an afternoon.\", steps = STEPS, className }: ScrollStoryProps) {\n  const [active, setActive] = React.useState(0)\n  const refs = React.useRef<(HTMLDivElement | null)[]>([])\n  const reduce = useReducedMotion()\n\n  React.useEffect(() => {\n    const io = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((e) => {\n          if (e.isIntersecting) setActive(Number((e.target as HTMLElement).dataset.index))\n        })\n      },\n      { rootMargin: \"-45% 0px -45% 0px\" }\n    )\n    refs.current.forEach((el) => el && io.observe(el))\n    return () => io.disconnect()\n  }, [steps.length])\n\n  return (\n    <section data-slot=\"scroll-story\" className={cn(\"mx-auto max-w-7xl px-4 py-24 sm:px-6 sm:py-32 lg:px-8\", className)}>\n      <div className=\"max-w-2xl\">\n        {eyebrow ? <p className=\"font-mono text-[11px] tracking-[0.08em] text-accent-fg uppercase\">{eyebrow}</p> : null}\n        <h2 className=\"mt-3 text-4xl leading-[1.05] font-medium tracking-[-0.04em] text-balance text-fg sm:text-5xl\">{title}</h2>\n      </div>\n      <div className=\"mt-12 grid gap-10 lg:grid-cols-2 lg:gap-16\">\n        <div className=\"relative\">\n          {/* rail */}\n          <div aria-hidden className=\"absolute top-2 bottom-2 left-[11px] hidden w-px bg-border lg:block\">\n            <motion.div\n              className=\"w-full origin-top bg-accent\"\n              animate={{ height: `${((active + 1) / steps.length) * 100}%` }}\n              transition={reduce ? { duration: 0 } : { type: \"spring\", bounce: 0.12, duration: 0.7 }}\n            />\n          </div>\n          {steps.map((s, i) => (\n            <div\n              key={s.title}\n              ref={(el) => {\n                refs.current[i] = el\n              }}\n              data-index={i}\n              className=\"relative flex min-h-[60vh] flex-col justify-center py-8 lg:pl-12\"\n            >\n              <span\n                aria-hidden\n                className={cn(\n                  \"absolute top-1/2 left-1.5 hidden size-[11px] -translate-y-1/2 rounded-full border-2 transition-colors duration-300 lg:block\",\n                  i <= active ? \"border-accent bg-accent\" : \"border-border-strong bg-bg\"\n                )}\n              />\n              <p className={cn(\"font-mono text-[11.5px] transition-colors duration-300\", i === active ? \"text-accent-fg\" : \"text-fg-subtle\")}>{s.kicker}</p>\n              <h3 className={cn(\"mt-2 text-2xl leading-8 font-medium tracking-[-0.03em] transition-colors duration-300 sm:text-3xl\", i === active ? \"text-fg\" : \"text-fg-subtle\")}>{s.title}</h3>\n              <p className=\"mt-3 max-w-md text-[15px] leading-[1.65] text-fg-muted\">{s.body}</p>\n              <div className=\"mt-6 lg:hidden\">{s.visual}</div>\n            </div>\n          ))}\n        </div>\n        <div className=\"hidden lg:block\">\n          <div className=\"sticky top-[20vh] grid h-[60vh] place-items-center rounded-3xl border border-border bg-sunken/50 bg-dots p-8 [--grid-size:18px]\">\n            <AnimatePresence mode=\"wait\" initial={false}>\n              <motion.div\n                key={active}\n                initial={reduce ? false : { opacity: 0, y: 16, filter: \"blur(6px)\" }}\n                animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, y: -12, filter: \"blur(6px)\", transition: { duration: 0.16 } }}\n                transition={{ duration: 0.4, ease: [0.2, 0, 0, 1] }}\n                className=\"grid w-full place-items-center\"\n              >\n                {steps[active]?.visual}\n              </motion.div>\n            </AnimatePresence>\n          </div>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { ScrollStory }\nexport type { ScrollStoryProps, Step }\n"
    }
  ]
}
