{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-progress",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": [],
  "devDependencies": [],
  "files": [
    {
      "path": "scroll-progress.tsx",
      "content": "\"use client\";\n\nimport { type RefObject, useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ScrollProgressPosition =\n  | \"left\"\n  | \"right\"\n  | \"bottom-left\"\n  | \"bottom-right\";\n\nconst POSITION_CLASSES: Record<ScrollProgressPosition, string> = {\n  left: \"left-0 top-1/2 -translate-y-1/2\",\n  right: \"right-0 top-1/2 -translate-y-1/2\",\n  \"bottom-left\": \"left-0 bottom-2\",\n  \"bottom-right\": \"right-0 bottom-2\",\n};\n\nconst LABEL_SIDE: Record<ScrollProgressPosition, \"left\" | \"right\"> = {\n  left: \"right\",\n  right: \"left\",\n  \"bottom-left\": \"right\",\n  \"bottom-right\": \"left\",\n};\n\nfunction getProgress(el: HTMLElement | null): number {\n  const scrollTop = el ? el.scrollTop : window.scrollY;\n  const scrollable = el\n    ? el.scrollHeight - el.clientHeight\n    : document.documentElement.scrollHeight -\n      document.documentElement.clientHeight;\n  if (scrollable <= 0) return 0;\n  return Math.min(100, Math.max(0, (scrollTop / scrollable) * 100));\n}\n\nfunction Ticks({ count, tone }: { count: number; tone: \"solid\" | \"muted\" }) {\n  return (\n    <div className=\"flex h-full flex-col justify-between\">\n      {Array.from({ length: count }, (_, i) => (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"block h-px w-full\",\n            tone === \"solid\" ? \"bg-foreground\" : \"bg-foreground/25\"\n          )}\n          key={i}\n        />\n      ))}\n    </div>\n  );\n}\n\nexport type ScrollProgressProps = {\n  position?: ScrollProgressPosition;\n  tickCount?: number;\n  height?: number;\n  width?: number;\n  showLabel?: boolean;\n  container?: RefObject<HTMLElement | null>;\n  className?: string;\n};\n\nexport function ScrollProgress({\n  position = \"right\",\n  tickCount = 40,\n  height = 160,\n  width = 14,\n  showLabel = true,\n  container,\n  className,\n}: ScrollProgressProps) {\n  const fillRef = useRef<HTMLDivElement>(null);\n  const labelWrapRef = useRef<HTMLDivElement>(null);\n  const labelTextRef = useRef<HTMLSpanElement>(null);\n  const labelSide = LABEL_SIDE[position];\n\n  useEffect(() => {\n    const target: HTMLElement | Window = container?.current ?? window;\n\n    const update = () => {\n      const pct = getProgress(container?.current ?? null);\n      const pctStr = `${pct}%`;\n      if (fillRef.current) fillRef.current.style.height = pctStr;\n      if (labelWrapRef.current) labelWrapRef.current.style.top = pctStr;\n      if (labelTextRef.current)\n        labelTextRef.current.textContent = String(Math.round(pct));\n    };\n\n    update();\n    target.addEventListener(\"scroll\", update, { passive: true });\n    window.addEventListener(\"resize\", update, { passive: true });\n\n    return () => {\n      target.removeEventListener(\"scroll\", update);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [container]);\n\n  return (\n    <div\n      className={cn(\n        \"pointer-events-none z-50 select-none\",\n        container ? \"absolute\" : \"fixed\",\n        POSITION_CLASSES[position],\n        className\n      )}\n      data-position={position}\n      data-slot=\"scroll-progress\"\n    >\n      <div className=\"relative\" style={{ height, width }}>\n        <Ticks count={tickCount} tone=\"muted\" />\n\n        <div\n          className=\"absolute inset-x-0 top-0 overflow-hidden\"\n          ref={fillRef}\n          style={{ height: \"0%\" }}\n        >\n          <div style={{ height, width: \"100%\" }}>\n            <Ticks count={tickCount} tone=\"solid\" />\n          </div>\n        </div>\n\n        {showLabel && (\n          <div\n            className={cn(\n              \"absolute flex items-center gap-1\",\n              labelSide === \"right\"\n                ? \"left-full -translate-y-1/2 pl-1.5\"\n                : \"right-full -translate-y-1/2 flex-row-reverse pr-1.5\"\n            )}\n            ref={labelWrapRef}\n            style={{ top: \"0%\" }}\n          >\n            <span aria-hidden=\"true\" className=\"h-px w-2 bg-foreground\" />\n            <span\n              className=\"font-medium text-foreground text-xs tabular-nums\"\n              ref={labelTextRef}\n            >\n              0\n            </span>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "Scroll Progress",
  "description": "Ruler-style scroll indicator with tick marks that fill in as the page scrolls and a live percentage readout, dockable to either side or bottom corner of the viewport or a scrollable container."
}
