{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "how-it-works-scroll-stepper",
  "title": "How It Works — Scroll Stepper",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/how-it-works-scroll-stepper/HowItWorksScrollStepper.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { ArrowRight } from 'lucide-vue-next'\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\nconst active = ref(0)\nconst root = ref<HTMLElement | null>(null)\nlet observer: IntersectionObserver | undefined\n\nonMounted(() => {\n  // Without IntersectionObserver the rail simply stays on the first step and\n  // the copy reads as a plain ordered list.\n  if (!('IntersectionObserver' in window) || !root.value) return\n  const targets = root.value.querySelectorAll<HTMLElement>('[data-step]')\n  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)) active.value = index\n      }\n    },\n    { rootMargin: '-45% 0px -45% 0px' },\n  )\n  targets.forEach((target) => observer?.observe(target))\n})\n\nonBeforeUnmount(() => observer?.disconnect())\n</script>\n\n<template>\n  <section ref=\"root\" data-slot=\"how-it-works-scroll-stepper\" class=\"bg-background\">\n    <div class=\"mx-auto max-w-6xl px-6 py-20 lg:py-28\">\n      <div class=\"max-w-2xl\">\n        <Badge variant=\"secondary\">Walkthrough</Badge>\n        <h2 class=\"mt-4 text-3xl font-semibold tracking-tight sm:text-4xl\">Scroll through the first week</h2>\n      </div>\n\n      <div class=\"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 class=\"lg:sticky lg:top-24 lg:self-start\">\n          <Card>\n            <CardContent class=\"p-5\">\n              <p class=\"text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase\">Progress</p>\n              <Progress\n                :model-value=\"((active + 1) / steps.length) * 100\"\n                class=\"mt-3 h-1.5\"\n                aria-label=\"Walkthrough progress\"\n              />\n              <ol class=\"mt-5 space-y-3\">\n                <li\n                  v-for=\"(step, index) in steps\"\n                  :key=\"step.id\"\n                  class=\"flex items-start gap-3 text-sm transition-colors\"\n                  :class=\"index === active ? 'text-foreground' : 'text-muted-foreground'\"\n                >\n                  <span\n                    class=\"mt-1 size-1.5 shrink-0 rounded-full transition-colors\"\n                    :class=\"index === active ? 'bg-primary' : 'bg-muted-foreground/40'\"\n                    aria-hidden=\"true\"\n                  />\n                  <span class=\"font-medium\">{{ step.title }}</span>\n                </li>\n              </ol>\n            </CardContent>\n          </Card>\n        </div>\n\n        <ol class=\"space-y-16 lg:space-y-28\">\n          <li v-for=\"(step, index) in steps\" :key=\"step.id\" :data-step=\"index\" class=\"scroll-mt-32\">\n            <p class=\"text-muted-foreground font-mono text-xs\">{{ String(index + 1).padStart(2, '0') }}</p>\n            <h3 class=\"mt-2 text-2xl font-semibold tracking-tight text-balance\">{{ step.title }}</h3>\n            <p class=\"text-muted-foreground mt-3 max-w-prose leading-relaxed\">{{ step.body }}</p>\n          </li>\n        </ol>\n      </div>\n\n      <div class=\"mt-12\">\n        <Button>\n          Start the walkthrough\n          <ArrowRight class=\"ml-2 size-4\" aria-hidden=\"true\" />\n        </Button>\n      </div>\n    </div>\n  </section>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/HowItWorksScrollStepper.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/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"
  ]
}