{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-progress",
  "title": "Scroll Progress",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/scroll-progress/ScrollProgress.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\n\nexport interface ScrollProgressProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Bar thickness in pixels. */\n  height?: number\n  /** Any CSS color or gradient — applied to `background` verbatim. */\n  color?: string\n  /** `fixed` pins to the viewport; `absolute` fills a positioned scrollable parent. */\n  position?: 'fixed' | 'absolute'\n  /** Scrollable element to measure. Defaults to the window/document. */\n  container?: HTMLElement | null\n  /** Lerp-smooth the displayed value toward the real progress each frame. */\n  smooth?: boolean\n}\n\nconst ScrollProgress = React.forwardRef<HTMLDivElement, ScrollProgressProps>(\n  (\n    {\n      className,\n      style,\n      height = 3,\n      color = 'var(--primary)',\n      position = 'fixed',\n      container = null,\n      smooth = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const [progress, setProgress] = React.useState(0)\n    const displayRef = React.useRef(0)\n    const targetRef = React.useRef(0)\n    const rafRef = React.useRef<number | null>(null)\n\n    // Read through a ref so toggling `smooth` does not re-attach listeners.\n    const smoothRef = React.useRef(smooth)\n    smoothRef.current = smooth\n\n    React.useEffect(() => {\n      if (typeof window === 'undefined') return\n\n      const clamp01 = (value: number) => Math.min(1, Math.max(0, value))\n      const reducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n      const read = () => {\n        if (container) {\n          const max = container.scrollHeight - container.clientHeight\n          return max > 0 ? clamp01(container.scrollTop / max) : 0\n        }\n        const doc = document.documentElement\n        const scrollTop = window.scrollY || doc.scrollTop || document.body.scrollTop || 0\n        const max = doc.scrollHeight - doc.clientHeight\n        return max > 0 ? clamp01(scrollTop / max) : 0\n      }\n\n      const stopLoop = () => {\n        if (rafRef.current !== null) {\n          cancelAnimationFrame(rafRef.current)\n          rafRef.current = null\n        }\n      }\n\n      const tick = () => {\n        displayRef.current += (targetRef.current - displayRef.current) * 0.18\n        setProgress(displayRef.current)\n        if (Math.abs(targetRef.current - displayRef.current) < 0.001) {\n          displayRef.current = targetRef.current\n          setProgress(targetRef.current)\n          rafRef.current = null\n          return\n        }\n        rafRef.current = requestAnimationFrame(tick)\n      }\n\n      const sync = () => {\n        targetRef.current = read()\n        if (!smoothRef.current || reducedMotion()) {\n          stopLoop()\n          displayRef.current = targetRef.current\n          setProgress(targetRef.current)\n          return\n        }\n        if (rafRef.current === null) rafRef.current = requestAnimationFrame(tick)\n      }\n\n      // EventTarget union keeps addEventListener compatible across HTMLElement | Window.\n      const bound: EventTarget = container ?? window\n      bound.addEventListener('scroll', sync, { passive: true })\n      window.addEventListener('resize', sync)\n\n      // Sync instantly on mount / container swap so the bar never animates up from zero.\n      targetRef.current = read()\n      displayRef.current = targetRef.current\n      setProgress(targetRef.current)\n\n      return () => {\n        bound.removeEventListener('scroll', sync)\n        window.removeEventListener('resize', sync)\n        stopLoop()\n      }\n    }, [container])\n\n    return (\n      <div\n        ref={ref}\n        data-uipkge=\"\"\n        data-slot=\"scroll-progress\"\n        aria-hidden=\"true\"\n        data-position={position}\n        data-smooth={smooth || undefined}\n        className={cn(\n          'pointer-events-none top-0 left-0 z-50 w-full origin-left',\n          position === 'fixed' ? 'fixed' : 'absolute',\n          className,\n        )}\n        style={{\n          height: `${height}px`,\n          background: color,\n          transform: `scaleX(${progress})`,\n          willChange: 'transform',\n          ...style,\n        }}\n        {...props}\n      />\n    )\n  },\n)\nScrollProgress.displayName = 'ScrollProgress'\n\nexport { ScrollProgress }\n",
      "type": "registry:ui",
      "target": "~/components/ui/scroll-progress/ScrollProgress.tsx"
    },
    {
      "path": "packages/registry-react/components/scroll-progress/index.ts",
      "content": "export { ScrollProgress, type ScrollProgressProps } from './ScrollProgress'\n",
      "type": "registry:ui",
      "target": "~/components/ui/scroll-progress/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Slim reading-progress bar pinned to the top of the viewport or of any supplied scrollable container. Scales horizontally with scroll depth; supports fixed and absolute (contained) modes, lerp smoothing with a prefers-reduced-motion fallback, custom thickness, and any CSS color or gradient.",
  "categories": [
    "feedback",
    "utility"
  ]
}