{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "scroll-progress",
  "title": "Scroll Progress",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/scroll-progress/ScrollProgress.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\n    class?: HTMLAttributes['class']\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  {\n    height: 3,\n    color: 'var(--primary)',\n    position: 'fixed',\n    container: null,\n    smooth: true,\n  },\n)\n\nconst progress = ref(0)\n\nlet display = 0\nlet target = 0\nlet rafId: number | null = null\nlet boundTarget: EventTarget | null = null\n\nfunction clamp01(value: number): number {\n  return Math.min(1, Math.max(0, value))\n}\n\nfunction prefersReducedMotion(): boolean {\n  return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n}\n\nfunction readProgress(): number {\n  if (props.container) {\n    const max = props.container.scrollHeight - props.container.clientHeight\n    return max > 0 ? clamp01(props.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\nfunction stopLoop() {\n  if (rafId !== null) {\n    cancelAnimationFrame(rafId)\n    rafId = null\n  }\n}\n\nfunction tick() {\n  display += (target - display) * 0.18\n  progress.value = display\n  if (Math.abs(target - display) < 0.001) {\n    display = target\n    progress.value = target\n    rafId = null\n    return\n  }\n  rafId = requestAnimationFrame(tick)\n}\n\nfunction sync() {\n  target = readProgress()\n  if (!props.smooth || prefersReducedMotion()) {\n    stopLoop()\n    display = target\n    progress.value = target\n    return\n  }\n  if (rafId === null) rafId = requestAnimationFrame(tick)\n}\n\nfunction detach() {\n  if (boundTarget) boundTarget.removeEventListener('scroll', sync)\n  boundTarget = null\n  if (typeof window !== 'undefined') window.removeEventListener('resize', sync)\n  stopLoop()\n}\n\nfunction attach() {\n  if (typeof window === 'undefined') return\n  detach()\n  boundTarget = props.container ?? 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()\n  display = target\n  progress.value = target\n}\n\nwatch(() => props.container, attach)\nonMounted(attach)\nonBeforeUnmount(detach)\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"scroll-progress\"\n    aria-hidden=\"true\"\n    :data-position=\"position\"\n    :data-smooth=\"smooth || undefined\"\n    :class=\"\n      cn(\n        'pointer-events-none top-0 left-0 z-50 w-full origin-left',\n        position === 'fixed' ? 'fixed' : 'absolute',\n        props.class,\n      )\n    \"\n    :style=\"{ height: `${height}px`, background: color, transform: `scaleX(${progress})`, willChange: 'transform' }\"\n  />\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/scroll-progress/ScrollProgress.vue"
    },
    {
      "path": "packages/registry-vue/components/scroll-progress/index.ts",
      "content": "export { default as ScrollProgress } from './ScrollProgress.vue'\n",
      "type": "registry:ui",
      "target": "~/app/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"
  ]
}