{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "feature-sticky-media-scroll",
  "title": "Features — Sticky Media Scroll",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/feature-sticky-media-scroll/feature-sticky-media-scroll.component.ts",
      "content": "// Deep-dive where the visual pins while the copy scrolls beside it, the pinned pane\n// swapping content as each section enters view, degrading to stacked pairs without\n// observers. Port of the React block 1:1.\nimport {\n  AfterViewInit,\n  ChangeDetectionStrategy,\n  Component,\n  ElementRef,\n  Input,\n  OnDestroy,\n  inject,\n  signal,\n} from '@angular/core'\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 { UiSeparatorComponent } from '@/ui/separator/separator.component'\n\nexport interface StickyMediaSection {\n  id: string\n  title: string\n  body: string\n  rows: { name: string; detail: string }[]\n}\n\nconst sections: StickyMediaSection[] = [\n  {\n    id: 'connect',\n    title: 'Point at the warehouse',\n    body: 'A read-only role is the whole integration. We mirror the schema, record freshness per table, and flag anything that looks abandoned before you model against it.',\n    rows: [\n      { name: 'warehouse.orders', detail: 'fresh · 2 min' },\n      { name: 'warehouse.refunds', detail: 'fresh · 2 min' },\n      { name: 'legacy.orders_v1', detail: 'stale · 41 days' },\n    ],\n  },\n  {\n    id: 'model',\n    title: 'Write the definition once',\n    body: 'Revenue, margin, and pipeline become files with owners. Every consumer resolves the same definition, so the exec summary and the close pack cannot disagree.',\n    rows: [\n      { name: 'revenue_net', detail: 'v128 · finance' },\n      { name: 'margin_by_channel', detail: 'v41 · finance' },\n      { name: 'pipeline_weighted', detail: 'v12 · revops' },\n    ],\n  },\n  {\n    id: 'serve',\n    title: 'Serve it everywhere',\n    body: 'Dashboards, scheduled exports, and the API read the same certified metric with the caller’s own scope applied, and each result carries the freshness it was served at.',\n    rows: [\n      { name: 'Exec summary', detail: 'live' },\n      { name: 'Finance close pack', detail: 'live' },\n      { name: 'Partner API', detail: 'live' },\n    ],\n  },\n]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-feature-sticky-media-scroll, [ui-feature-sticky-media-scroll]',\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: [UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiSeparatorComponent],\n  template: `\n    <section data-slot=\"feature-sticky-media-scroll\" [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\">How it fits together</span>\n          <h2 class=\"mt-4 text-3xl font-semibold tracking-tight sm:text-4xl\">Three moves, in order</h2>\n        </div>\n\n        <div class=\"mt-12 grid gap-10 lg:grid-cols-2 lg:gap-16\">\n          <div class=\"space-y-20 lg:space-y-32\">\n            @for (section of allSections; track section.id; let i = $index) {\n              <div [attr.data-section]=\"i\" class=\"scroll-mt-32\">\n                <p class=\"text-muted-foreground font-mono text-xs\">{{ pad(i + 1) }}</p>\n                <h3 class=\"mt-2 text-2xl font-semibold tracking-tight text-balance\">{{ section.title }}</h3>\n                <p class=\"text-muted-foreground mt-3 max-w-prose leading-relaxed\">{{ section.body }}</p>\n\n                <!-- Inline card below the breakpoint, where nothing can pin usefully. -->\n                <div ui-card class=\"mt-5 lg:hidden\">\n                  <div ui-card-content class=\"p-0\">\n                    <ul class=\"divide-border divide-y\">\n                      @for (row of section.rows; track row.name) {\n                        <li class=\"flex items-center justify-between gap-3 px-4 py-2.5\">\n                          <span class=\"min-w-0 truncate font-mono text-xs\">{{ row.name }}</span>\n                          <span class=\"text-muted-foreground shrink-0 text-xs\">{{ row.detail }}</span>\n                        </li>\n                      }\n                    </ul>\n                  </div>\n                </div>\n              </div>\n            }\n          </div>\n\n          <div class=\"hidden lg:sticky lg:top-24 lg:block lg:h-fit\">\n            <div ui-card>\n              <div ui-card-content class=\"p-0\">\n                <div class=\"border-border text-muted-foreground border-b px-4 py-2.5 font-mono text-xs\">\n                  {{ allSections[active()]!.title }}\n                </div>\n                <ul class=\"divide-border min-h-[9rem] divide-y\">\n                  @for (row of allSections[active()]!.rows; track row.name) {\n                    <li class=\"flex items-center justify-between gap-3 px-4 py-3\">\n                      <span class=\"min-w-0 truncate font-mono text-xs\">{{ row.name }}</span>\n                      <span class=\"text-muted-foreground shrink-0 text-xs\">{{ row.detail }}</span>\n                    </li>\n                  }\n                </ul>\n                <ui-separator />\n                <p class=\"text-muted-foreground px-4 py-2.5 text-xs\">\n                  Step {{ active() + 1 }} of {{ allSections.length }}\n                </p>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <button ui-button variant=\"outline\" class=\"mt-12\">Start the walkthrough</button>\n      </div>\n    </section>\n  `,\n})\nexport class UiFeatureStickyMediaScrollComponent implements AfterViewInit, OnDestroy {\n  protected readonly allSections = sections\n\n  private observer?: IntersectionObserver\n\n  @Input() set initialActive(v: number) {\n    if (Number.isInteger(v) && v >= 0 && v < sections.length) this.active.set(v)\n  }\n  @Input('class') className?: string\n\n  readonly active = signal(0)\n\n  private readonly host = inject(ElementRef<HTMLElement>)\n\n  ngAfterViewInit(): void {\n    // Without IntersectionObserver the pinned pane simply stays on the first\n    // section and the page reads as stacked copy-and-visual pairs.\n    if (typeof IntersectionObserver === 'undefined') return\n    const targets = this.host.nativeElement.querySelectorAll('[data-section]')\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['section'])\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  setActive(index: number): void {\n    if (index >= 0 && index < sections.length) this.active.set(index)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/feature-sticky-media-scroll/feature-sticky-media-scroll.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/feature-sticky-media-scroll/index.ts",
      "content": "export { UiFeatureStickyMediaScrollComponent, type StickyMediaSection } from './feature-sticky-media-scroll.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/feature-sticky-media-scroll/index.ts"
    }
  ],
  "dependencies": [],
  "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/separator.json"
  ],
  "description": "Deep-dive where the visual pins while the copy scrolls beside it, the pinned pane swapping content as each section enters view, degrading to stacked pairs without observers.",
  "categories": [
    "marketing"
  ]
}