{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "feature-sticky-media-scroll",
  "title": "Features — Sticky Media Scroll",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/feature-sticky-media-scroll/FeatureStickyMediaScroll.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Separator } from '@/components/ui/separator'\n\nconst sections = [\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\nconst active = ref(0)\nconst root = ref<HTMLElement | null>(null)\nlet observer: IntersectionObserver | undefined\n\nonMounted(() => {\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 (!('IntersectionObserver' in window) || !root.value) return\n  const targets = root.value.querySelectorAll<HTMLElement>('[data-section]')\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.section)\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=\"feature-sticky-media-scroll\" 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\">How it fits together</Badge>\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          <div v-for=\"(section, index) in sections\" :key=\"section.id\" :data-section=\"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\">{{ 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            <Card class=\"mt-5 lg:hidden\">\n              <CardContent class=\"p-0\">\n                <ul class=\"divide-border divide-y\">\n                  <li\n                    v-for=\"row in section.rows\"\n                    :key=\"row.name\"\n                    class=\"flex items-center justify-between gap-3 px-4 py-2.5\"\n                  >\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                </ul>\n              </CardContent>\n            </Card>\n          </div>\n        </div>\n\n        <div class=\"hidden lg:sticky lg:top-24 lg:block lg:h-fit\">\n          <Card>\n            <CardContent class=\"p-0\">\n              <div class=\"border-border text-muted-foreground border-b px-4 py-2.5 font-mono text-xs\">\n                {{ sections[active].title }}\n              </div>\n              <ul class=\"divide-border min-h-[9rem] divide-y\">\n                <li\n                  v-for=\"row in sections[active].rows\"\n                  :key=\"row.name\"\n                  class=\"flex items-center justify-between gap-3 px-4 py-3\"\n                >\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              </ul>\n              <Separator />\n              <p class=\"text-muted-foreground px-4 py-2.5 text-xs\">Step {{ active + 1 }} of {{ sections.length }}</p>\n            </CardContent>\n          </Card>\n        </div>\n      </div>\n\n      <Button variant=\"outline\" class=\"mt-12\">Start the walkthrough</Button>\n    </div>\n  </section>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/FeatureStickyMediaScroll.vue"
    }
  ],
  "dependencies": [],
  "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/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"
  ]
}