{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "scroll-progress",
  "title": "Scroll Progress",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/scroll-progress/ScrollProgress.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollProgressProps extends 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    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    height = 3,\n    color = 'var(--primary)',\n    position = 'fixed',\n    container = null,\n    smooth = true,\n    ref = $bindable(null),\n    ...restProps\n  }: ScrollProgressProps = $props()\n\n  let progress = $state(0)\n\n  let display = 0\n  let target = 0\n  let rafId: number | null = null\n  let boundTarget: EventTarget | null = null\n\n  function clamp01(value: number): number {\n    return Math.min(1, Math.max(0, value))\n  }\n\n  function prefersReducedMotion(): boolean {\n    return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n  }\n\n  function readProgress(el: HTMLElement | null): number {\n    if (el) {\n      const max = el.scrollHeight - el.clientHeight\n      return max > 0 ? clamp01(el.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  function stopLoop() {\n    if (rafId !== null) {\n      cancelAnimationFrame(rafId)\n      rafId = null\n    }\n  }\n\n  function tick() {\n    display += (target - display) * 0.18\n    progress = display\n    if (Math.abs(target - display) < 0.001) {\n      display = target\n      progress = target\n      rafId = null\n      return\n    }\n    rafId = requestAnimationFrame(tick)\n  }\n\n  function detach() {\n    if (boundTarget) boundTarget.removeEventListener('scroll', sync)\n    boundTarget = null\n    if (typeof window !== 'undefined') window.removeEventListener('resize', sync)\n    stopLoop()\n  }\n\n  function sync() {\n    target = readProgress(container)\n    if (!smooth || prefersReducedMotion()) {\n      stopLoop()\n      display = target\n      progress = target\n      return\n    }\n    if (rafId === null) rafId = requestAnimationFrame(tick)\n  }\n\n  $effect(() => {\n    if (typeof window === 'undefined') return\n    // Track `container` so a swap re-binds the scroll listener.\n    const el = container\n    detach()\n    boundTarget = el ?? window\n    boundTarget.addEventListener('scroll', sync, { passive: true })\n    window.addEventListener('resize', sync)\n    // Sync instantly on mount / container swap so the bar never animates up from zero.\n    target = readProgress(el)\n    display = target\n    progress = target\n    return () => detach()\n  })\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge\n  data-slot=\"scroll-progress\"\n  aria-hidden=\"true\"\n  data-position={position}\n  data-smooth={smooth || undefined}\n  class={cn('pointer-events-none top-0 left-0 z-50 w-full origin-left', position === 'fixed' ? 'fixed' : 'absolute', className)}\n  style=\"height: {height}px; background: {color}; transform: scaleX({progress}); will-change: transform;\"\n  {...restProps}\n></div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-progress/ScrollProgress.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-progress/index.ts",
      "content": "export { default as ScrollProgress, type ScrollProgressProps } from './ScrollProgress.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/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"
  ]
}