{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "how-it-works-scroll-stepper",
  "title": "How It Works — Scroll Stepper",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/how-it-works-scroll-stepper/HowItWorksScrollStepper.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport { ArrowRight } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Progress } from '@/components/ui/progress'\n\nconst steps = [\n  {\n    id: 'connect',\n    title: 'Connect the warehouse',\n    body: 'A read-only role and the account URL. We mirror the schema and catalogue what is there — no agent, no extract, no copy of your data leaving the account.',\n  },\n  {\n    id: 'define',\n    title: 'Define the metrics that matter',\n    body: 'Start with the numbers that already appear in the board deck. Each becomes a versioned definition with an owner, reviewed like any other change in the repo.',\n  },\n  {\n    id: 'certify',\n    title: 'Certify against history',\n    body: 'Every definition is reconciled against the prior four quarters before it can be published. Mismatches surface as a diff, not as a surprise in the close.',\n  },\n  {\n    id: 'publish',\n    title: 'Publish once, everywhere',\n    body: 'Dashboards, scheduled exports, and the API all resolve the same definition. Changing it updates every consumer, and reverting is the same one click.',\n  },\n]\n\nexport function HowItWorksScrollStepper() {\n  const [active, setActive] = useState(0)\n  const root = useRef<HTMLElement | null>(null)\n\n  useEffect(() => {\n    // Without IntersectionObserver the rail simply stays on the first step and\n    // the copy reads as a plain ordered list.\n    if (typeof IntersectionObserver === 'undefined' || !root.current) return\n    const targets = root.current.querySelectorAll<HTMLElement>('[data-step]')\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (!entry.isIntersecting) continue\n          const index = Number((entry.target as HTMLElement).dataset.step)\n          if (!Number.isNaN(index)) setActive(index)\n        }\n      },\n      { rootMargin: '-45% 0px -45% 0px' },\n    )\n    targets.forEach((target) => observer.observe(target))\n    return () => observer.disconnect()\n  }, [])\n\n  return (\n    <section ref={root} data-slot=\"how-it-works-scroll-stepper\" className=\"bg-background\">\n      <div className=\"mx-auto max-w-6xl px-6 py-20 lg:py-28\">\n        <div className=\"max-w-2xl\">\n          <Badge variant=\"secondary\">Walkthrough</Badge>\n          <h2 className=\"mt-4 text-3xl font-semibold tracking-tight sm:text-4xl\">Scroll through the first week</h2>\n        </div>\n\n        <div className=\"mt-12 grid gap-10 lg:grid-cols-[18rem_1fr] lg:gap-16\">\n          {/* Sticky rail tracks whichever step is nearest the viewport middle. */}\n          <div className=\"lg:sticky lg:top-24 lg:self-start\">\n            <Card>\n              <CardContent className=\"p-5\">\n                <p className=\"text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase\">Progress</p>\n                <Progress\n                  value={((active + 1) / steps.length) * 100}\n                  className=\"mt-3 h-1.5\"\n                  aria-label=\"Walkthrough progress\"\n                />\n                <ol className=\"mt-5 space-y-3\">\n                  {steps.map((step, index) => (\n                    <li\n                      key={step.id}\n                      className={`flex items-start gap-3 text-sm transition-colors ${\n                        index === active ? 'text-foreground' : 'text-muted-foreground'\n                      }`}\n                    >\n                      <span\n                        className={`mt-1 size-1.5 shrink-0 rounded-full transition-colors ${\n                          index === active ? 'bg-primary' : 'bg-muted-foreground/40'\n                        }`}\n                        aria-hidden=\"true\"\n                      />\n                      <span className=\"font-medium\">{step.title}</span>\n                    </li>\n                  ))}\n                </ol>\n              </CardContent>\n            </Card>\n          </div>\n\n          <ol className=\"space-y-16 lg:space-y-28\">\n            {steps.map((step, index) => (\n              <li key={step.id} data-step={index} className=\"scroll-mt-32\">\n                <p className=\"text-muted-foreground font-mono text-xs\">{String(index + 1).padStart(2, '0')}</p>\n                <h3 className=\"mt-2 text-2xl font-semibold tracking-tight text-balance\">{step.title}</h3>\n                <p className=\"text-muted-foreground mt-3 max-w-prose leading-relaxed\">{step.body}</p>\n              </li>\n            ))}\n          </ol>\n        </div>\n\n        <div className=\"mt-12\">\n          <Button>\n            Start the walkthrough\n            <ArrowRight className=\"ml-2 size-4\" aria-hidden=\"true\" />\n          </Button>\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/HowItWorksScrollStepper.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/card.json",
    "https://uipkge.dev/r/react/progress.json"
  ],
  "description": "Scroll-driven walkthrough where a sticky progress rail tracks which step is in view while the step copy scrolls past, falling back to a plain list without IntersectionObserver.",
  "categories": [
    "marketing"
  ]
}