{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-progress-indicator",
  "title": "Scroll Progress Indicator",
  "description": "A circular scroll progress indicator that doubles as a compact return-to-top control.",
  "dependencies": [
    "@solar-icons/react"
  ],
  "files": [
    {
      "path": "components/content/ScrollCompass.tsx",
      "content": "'use client';\n\nimport { useEffect, useState, type RefObject } from 'react';\nimport { ArrowUp } from 'lucide-react';\n\ntype ScrollCompassProps = {\n  target?: RefObject<HTMLElement | null>;\n  progress?: number;\n  position?: 'left' | 'right' | 'inline' | 'absolute';\n  size?: number;\n  scrollToTop?: boolean;\n  className?: string;\n};\n\nconst clamp = (value: number) => Math.min(1, Math.max(0, value));\n\nexport default function ScrollCompass({\n  target,\n  progress,\n  position = 'right',\n  size = 64,\n  scrollToTop = true,\n  className = '',\n}: ScrollCompassProps) {\n  const controlled = typeof progress === 'number';\n  const [value, setValue] = useState(() => clamp(progress ?? 0));\n  const percentage = Math.round(value * 100);\n  const circumference = 2 * Math.PI * 42;\n\n  useEffect(() => {\n    if (controlled) {\n      setValue(clamp(progress ?? 0));\n      return;\n    }\n\n    const update = () => {\n      const element = target?.current;\n      const height = element\n        ? element.scrollHeight - element.clientHeight\n        : document.documentElement.scrollHeight - window.innerHeight;\n      const offset = element ? element.scrollTop : window.scrollY;\n      setValue(height > 0 ? clamp(offset / height) : 0);\n    };\n\n    update();\n    const element = target?.current;\n    const source = element ?? window;\n    source.addEventListener('scroll', update, { passive: true });\n    window.addEventListener('resize', update);\n    return () => {\n      source.removeEventListener('scroll', update);\n      window.removeEventListener('resize', update);\n    };\n  }, [controlled, progress, target]);\n\n  const placement =\n    position === 'left'\n      ? 'fixed bottom-6 left-6 z-50'\n      : position === 'right'\n        ? 'fixed bottom-6 right-6 z-50'\n        : position === 'absolute'\n          ? 'absolute bottom-10 right-10 z-10'\n          : 'relative';\n\n  return (\n    <button\n      type='button'\n      className={[\n        placement,\n        'group grid place-items-center rounded-full bg-[var(--color-background,#fff)] shadow-[0_12px_32px_rgb(15_23_42/0.16)] ring-1 ring-[color:var(--color-border,#e2e8f0)] transition-transform hover:-translate-y-0.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring,#6366f1)] focus-visible:ring-offset-2',\n        className,\n      ].join(' ')}\n      style={{ width: size, height: size }}\n      aria-label={`Scroll progress: ${percentage}%.${scrollToTop ? ' Return to top.' : ''}`}\n      onClick={() => {\n        if (!scrollToTop) return;\n        const element = target?.current;\n        if (element) element.scrollTo({ top: 0, behavior: 'smooth' });\n        else window.scrollTo({ top: 0, behavior: 'smooth' });\n      }}\n    >\n      <svg aria-hidden='true' viewBox='0 0 100 100' className='absolute inset-1 h-[calc(100%-0.5rem)] w-[calc(100%-0.5rem)] -rotate-90'>\n        <circle cx='50' cy='50' r='42' fill='none' stroke='currentColor' strokeWidth='7' className='text-[color:var(--color-muted,#f1f5f9)]' />\n        <circle\n          cx='50'\n          cy='50'\n          r='42'\n          fill='none'\n          stroke='currentColor'\n          strokeWidth='7'\n          strokeLinecap='round'\n          className='text-[color:var(--color-primary,#4f46e5)] transition-[stroke-dashoffset] duration-150'\n          strokeDasharray={circumference}\n          strokeDashoffset={circumference * (1 - value)}\n        />\n      </svg>\n      <span\n        className={`relative text-xs font-semibold tabular-nums text-[color:var(--color-foreground,#1e293b)] transition-opacity ${scrollToTop ? 'group-hover:opacity-0 group-focus-visible:opacity-0' : ''\n          }`}\n      >\n        {percentage}%\n      </span>\n      {scrollToTop ? (\n        <ArrowUp\n          aria-hidden='true'\n          className='absolute size-5 scale-75 text-[color:var(--color-foreground,#1e293b)] opacity-0 transition-[opacity,transform] group-hover:scale-100 group-hover:opacity-100 group-focus-visible:scale-100 group-focus-visible:opacity-100'\n        />\n      ) : null}\n      {scrollToTop ? <span className='sr-only'>Return to top</span> : null}\n    </button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/content/ScrollCompass.tsx"
    }
  ],
  "categories": [
    "content"
  ],
  "type": "registry:component"
}