{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sticky-scroll",
  "title": "Sticky Scroll",
  "description": "Copy scrolls past a pinned panel that swaps per section.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/sticky-scroll.tsx",
      "content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport {\n  motion,\n  useMotionValueEvent,\n  useReducedMotion,\n  useScroll,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface StickyScrollItem {\n  title: string;\n  description: string;\n  /** Rendered in the pinned panel while this item is active */\n  content?: React.ReactNode;\n}\n\ninterface StickyScrollProps {\n  items: StickyScrollItem[];\n  /** Scrollable element to track instead of the window */\n  container?: React.RefObject<HTMLElement | null>;\n  className?: string;\n}\n\n/**\n * Long-form copy that scrolls past a pinned panel; the panel swaps as each\n * section takes over. The classic \"how it works\" section. Heights use `cqh`,\n * which resolves against the viewport unless an ancestor is a size container\n * (e.g. a fixed-height scroll box with `@container-size`).\n */\nexport function StickyScroll({ items, container, className }: StickyScrollProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const [active, setActive] = useState(0);\n  const reducedMotion = useReducedMotion();\n  const duration = reducedMotion ? 0 : 0.3;\n  const { scrollYProgress } = useScroll({\n    container,\n    target: ref,\n    offset: [\"start start\", \"end end\"],\n  });\n\n  useMotionValueEvent(scrollYProgress, \"change\", (progress) => {\n    if (!items.length) return;\n    // Split the scrollable range into one band per item.\n    const next = Math.min(\n      items.length - 1,\n      Math.floor(progress * items.length)\n    );\n    setActive(next);\n  });\n\n  if (!items.length) return null;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"sticky-scroll\"\n      className={cn(\"relative flex gap-12\", className)}\n    >\n      <div className=\"flex-1 py-[30cqh]\">\n        {items.map((item, i) => (\n          <div key={item.title} className=\"flex min-h-[60cqh] flex-col justify-center\">\n            {/* Only the large title dims, and only to a level that keeps\n                3:1 contrast; the description stays at full strength. */}\n            <motion.h3\n              animate={{ opacity: active === i ? 1 : 0.6 }}\n              transition={{ duration }}\n              className=\"text-2xl font-semibold\"\n            >\n              {item.title}\n            </motion.h3>\n            <p className=\"mt-3 max-w-md text-muted-foreground\">\n              {item.description}\n            </p>\n          </div>\n        ))}\n      </div>\n\n      <div className=\"sticky top-[20cqh] hidden h-[60cqh] flex-1 lg:block\">\n        <div className=\"relative size-full overflow-hidden rounded-2xl border bg-gradient-to-br from-brand-from/10 to-brand-to/10\">\n          {items.map((item, i) => (\n            <motion.div\n              key={item.title}\n              initial={false}\n              animate={{\n                opacity: active === i ? 1 : 0,\n                scale: active === i ? 1 : 0.97,\n              }}\n              transition={{ duration, ease: \"easeOut\" }}\n              aria-hidden={active !== i}\n              inert={active !== i}\n              className=\"absolute inset-0 flex items-center justify-center p-8\"\n            >\n              {item.content ?? (\n                <span className=\"text-xl font-medium\">{item.title}</span>\n              )}\n            </motion.div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/sticky-scroll.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-brand": "var(--brand)",
      "color-brand-from": "var(--brand-from)",
      "color-brand-via": "var(--brand-via)",
      "color-brand-to": "var(--brand-to)"
    },
    "light": {
      "brand": "oklch(0.546 0.245 263)",
      "brand-from": "oklch(0.546 0.245 263)",
      "brand-via": "oklch(0.623 0.214 260)",
      "brand-to": "oklch(0.685 0.169 237)"
    },
    "dark": {
      "brand": "oklch(0.623 0.214 260)",
      "brand-from": "oklch(0.623 0.214 260)",
      "brand-via": "oklch(0.707 0.165 255)",
      "brand-to": "oklch(0.746 0.16 233)"
    }
  },
  "type": "registry:ui"
}