{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "how-it-works-scroll-stepper",
  "title": "How It Works — Scroll Stepper",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/how-it-works-scroll-stepper/how-it-works-scroll-stepper.component.ts",
      "content": "// Scroll-driven walkthrough where a sticky progress rail tracks which step is in view\n// while the step copy scrolls past, falling back to a plain list without IntersectionObserver.\n// Port of the React block 1:1.\nimport {\n  AfterViewInit,\n  ChangeDetectionStrategy,\n  Component,\n  ElementRef,\n  Input,\n  OnDestroy,\n  computed,\n  inject,\n  signal,\n} from '@angular/core'\nimport { ArrowRight, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\n\nexport interface ScrollStepperStep {\n  id: string\n  title: string\n  body: string\n}\n\nconst steps: ScrollStepperStep[] = [\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\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-how-it-works-scroll-stepper, [ui-how-it-works-scroll-stepper]',\n  standalone: true,\n  // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiProgressComponent,\n  ],\n  template: `\n    <section data-slot=\"how-it-works-scroll-stepper\" [class]=\"rootClass\">\n      <div class=\"mx-auto max-w-6xl px-6 py-20 lg:py-28\">\n        <div class=\"max-w-2xl\">\n          <span ui-badge variant=\"secondary\">Walkthrough</span>\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            <div ui-card>\n              <div ui-card-content class=\"p-5\">\n                <p class=\"text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase\">Progress</p>\n                <ui-progress [value]=\"progress()\" class=\"mt-3 h-1.5\" aria-label=\"Walkthrough progress\" />\n                <ol class=\"mt-5 space-y-3\">\n                  @for (step of allSteps; track step.id; let index = $index) {\n                    <li [class]=\"railItemClass(index)\">\n                      <span [class]=\"railDotClass(index)\" aria-hidden=\"true\"></span>\n                      <span class=\"font-medium\">{{ step.title }}</span>\n                    </li>\n                  }\n                </ol>\n              </div>\n            </div>\n          </div>\n\n          <ol class=\"space-y-16 lg:space-y-28\">\n            @for (step of allSteps; track step.id; let index = $index) {\n              <li [attr.data-step]=\"index\" class=\"scroll-mt-32\">\n                <p class=\"text-muted-foreground font-mono text-xs\">{{ pad(index + 1) }}</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            }\n          </ol>\n        </div>\n\n        <div class=\"mt-12\">\n          <button ui-button>\n            Start the walkthrough\n            <lucide-icon [img]=\"ArrowRight\" class=\"ml-2 size-4\" aria-hidden=\"true\" />\n          </button>\n        </div>\n      </div>\n    </section>\n  `,\n})\nexport class UiHowItWorksScrollStepperComponent implements AfterViewInit, OnDestroy {\n  protected readonly ArrowRight = ArrowRight\n  protected readonly allSteps = steps\n\n  private readonly host = inject(ElementRef<HTMLElement>)\n  private observer?: IntersectionObserver\n\n  @Input() set initialActive(v: number) {\n    if (Number.isInteger(v) && v >= 0 && v < steps.length) this.active.set(v)\n  }\n  @Input('class') className?: string\n\n  readonly active = signal(0)\n\n  readonly progress = computed(() => ((this.active() + 1) / steps.length) * 100)\n\n  ngAfterViewInit(): void {\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') return\n    const targets = this.host.nativeElement.querySelectorAll('[data-step]')\n    if (targets.length === 0) return\n    this.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)) this.active.set(index)\n        }\n      },\n      { rootMargin: '-45% 0px -45% 0px' },\n    )\n    targets.forEach((target: Element) => this.observer?.observe(target))\n  }\n\n  ngOnDestroy(): void {\n    this.observer?.disconnect()\n  }\n\n  get rootClass(): string {\n    return cn('bg-background', this.className)\n  }\n\n  pad(n: number): string {\n    return String(n).padStart(2, '0')\n  }\n\n  railItemClass(index: number): string {\n    return cn(\n      'flex items-start gap-3 text-sm transition-colors',\n      index === this.active() ? 'text-foreground' : 'text-muted-foreground',\n    )\n  }\n\n  railDotClass(index: number): string {\n    return cn(\n      'mt-1 size-1.5 shrink-0 rounded-full transition-colors',\n      index === this.active() ? 'bg-primary' : 'bg-muted-foreground/40',\n    )\n  }\n\n  setActive(index: number): void {\n    if (index >= 0 && index < steps.length) this.active.set(index)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/how-it-works-scroll-stepper/how-it-works-scroll-stepper.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/how-it-works-scroll-stepper/index.ts",
      "content": "export { UiHowItWorksScrollStepperComponent, type ScrollStepperStep } from './how-it-works-scroll-stepper.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/how-it-works-scroll-stepper/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/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"
  ]
}