{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-scene",
  "title": "Scroll Scene",
  "description": "Turns the scrolling of a tall element into a number, without rendering the page to do it.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/scroll-scene/scroll-scene.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ScrollRange = \"cover\" | \"contain\" | \"enter\" | \"exit\"\n\nexport type ScrollSceneProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onProgress\"\n> & {\n  /**\n   * Which span of scrolling maps to nought through one. Cover runs from the\n   * moment the element appears to the moment it has gone.\n   */\n  range?: ScrollRange\n  /**\n   * Called on every frame the element is on screen. This is deliberately not\n   * React state: a scene reading it redraws itself, and the page does not.\n   */\n  onProgress?: (progress: number) => void\n  /** Pins the children to the viewport while the scene scrolls past them. */\n  sticky?: boolean\n  contentClassName?: string\n}\n\nfunction progressFor(\n  range: ScrollRange,\n  rect: DOMRect,\n  viewport: number\n): number {\n  switch (range) {\n    case \"enter\":\n      return (viewport - rect.top) / Math.max(rect.height, 1)\n    case \"exit\":\n      return -rect.top / Math.max(rect.height, 1)\n    case \"contain\": {\n      const span = Math.abs(viewport - rect.height)\n      return span === 0\n        ? 0\n        : (viewport - rect.top - Math.min(rect.height, viewport)) / span\n    }\n    default:\n      return (viewport - rect.top) / Math.max(viewport + rect.height, 1)\n  }\n}\n\n/**\n * Turns the scrolling of a tall element into a number between nought and one,\n * published as the `--scroll-progress` custom property and to a callback. A\n * CSS effect can read the property and a canvas can read the callback, and\n * neither of them causes a render.\n */\nexport function ScrollScene({\n  range = \"cover\",\n  onProgress,\n  sticky = false,\n  className,\n  contentClassName,\n  children,\n  ...rootProps\n}: ScrollSceneProps) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const report = React.useRef(onProgress)\n\n  React.useEffect(() => {\n    report.current = onProgress\n  })\n\n  React.useEffect(() => {\n    const element = ref.current\n    if (!element) return\n\n    let raf = 0\n    let visible = false\n    let last = -1\n\n    const measure = () => {\n      const rect = element.getBoundingClientRect()\n      const progress = Math.min(\n        1,\n        Math.max(0, progressFor(range, rect, window.innerHeight))\n      )\n\n      if (progress !== last) {\n        last = progress\n        element.style.setProperty(\"--scroll-progress\", progress.toFixed(4))\n        element.dataset.scroll =\n          progress <= 0 ? \"before\" : progress >= 1 ? \"after\" : \"active\"\n        report.current?.(progress)\n      }\n    }\n\n    const loop = () => {\n      measure()\n      raf = window.requestAnimationFrame(loop)\n    }\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        const next = entry?.isIntersecting ?? false\n        if (next === visible) return\n        visible = next\n\n        if (visible) {\n          if (raf === 0) raf = window.requestAnimationFrame(loop)\n        } else {\n          if (raf !== 0) window.cancelAnimationFrame(raf)\n          raf = 0\n          // One last reading, so a scene left behind holds an end value\n          // rather than whatever it had when it went out of view.\n          measure()\n        }\n      },\n      { rootMargin: \"64px\" }\n    )\n\n    observer.observe(element)\n    measure()\n\n    return () => {\n      observer.disconnect()\n      if (raf !== 0) window.cancelAnimationFrame(raf)\n    }\n  }, [range])\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"scroll-scene\"\n      className={cn(\"relative\", className)}\n      {...rootProps}\n    >\n      <div\n        className={cn(\n          sticky && \"sticky top-0 h-screen overflow-hidden\",\n          contentClassName\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "scroll",
    "motion",
    "progress"
  ],
  "type": "registry:ui"
}