{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-progress",
  "type": "registry:component",
  "title": "Scroll progress",
  "description": "Reading progress as a thin bar that fills along the top edge and a ring with the percent inside, for the page or any scrollable element.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/scroll-progress.tsx",
      "type": "registry:ui",
      "target": "components/ui/scroll-progress.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { motion, useMotionValueEvent, useReducedMotion, useScroll, useSpring, useTransform, type MotionValue } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Reading progress as a thin bar that fills along the top edge and a ring with the percent inside, for the page or any scrollable element.\n * Pin it to the viewport with `fixed`, point it at a scroller with\n * `container`, or give it children and it becomes its own scrolling panel.\n * The spring gives the bar a little weight as you scroll; reduced motion\n * tracks the scroll position exactly.\n */\ntype ScrollProgressProps = {\n  /** Which indicators to show. */\n  variant?: \"bar\" | \"circle\" | \"both\"\n  /** A scrollable element to track instead of the window. The indicators render where you put them, inside a `relative` wrapper. */\n  container?: React.RefObject<HTMLElement | null>\n  /** Pin the indicators to the viewport and track the page (or `container`). Nothing else renders. */\n  fixed?: boolean\n  /** Bar thickness in pixels. */\n  height?: number\n  /** Ring diameter in pixels. */\n  size?: number\n  /** Without `fixed` or `container`, the component scrolls this content in its own panel. */\n  children?: React.ReactNode\n  /** Panel header title. */\n  title?: React.ReactNode\n  /** Panel header line under the title. */\n  subtitle?: React.ReactNode\n  className?: string\n}\n\n/** The release spring: quick enough to keep up with the scroll, soft enough to glide. */\nconst SPRING = { stiffness: 400, damping: 40, restDelta: 0.001 }\n\nfunction Bar({ value, height, percent, className }: { value: MotionValue<number>; height: number; percent?: number; className?: string }) {\n  // The bar is only announced when it is the sole indicator; otherwise the ring carries the role.\n  const a11y = percent === undefined ? { \"aria-hidden\": true } : { role: \"progressbar\", \"aria-label\": \"Reading progress\", \"aria-valuemin\": 0, \"aria-valuemax\": 100, \"aria-valuenow\": percent }\n  return <motion.div data-slot=\"scroll-progress-bar\" {...a11y} className={cn(\"origin-left bg-accent\", className)} style={{ height, scaleX: value }} />\n}\n\nfunction Ring({ value, size, percent, className }: { value: MotionValue<number>; size: number; percent: number; className?: string }) {\n  const label = useTransform(value, (v) => `${Math.round(Math.min(1, Math.max(0, v)) * 100)}%`)\n  // Round caps would draw a dot at zero, so the arc only appears once there is something to show.\n  const arcOpacity = useTransform(value, [0, 0.005], [0, 1])\n  const stroke = 3\n  const r = (size - stroke) / 2\n  return (\n    <div\n      data-slot=\"scroll-progress-ring\"\n      role=\"progressbar\"\n      aria-label=\"Reading progress\"\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={percent}\n      className={cn(\"relative grid place-items-center rounded-full bg-surface shadow-raised\", className)}\n      style={{ width: size, height: size }}\n    >\n      <svg aria-hidden className=\"absolute inset-0 -rotate-90\" width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill=\"none\">\n        <circle cx={size / 2} cy={size / 2} r={r} className=\"stroke-border\" strokeWidth={stroke} />\n        <motion.circle cx={size / 2} cy={size / 2} r={r} className=\"stroke-accent\" strokeWidth={stroke} strokeLinecap=\"round\" pathLength={1} style={{ pathLength: value, opacity: arcOpacity }} />\n      </svg>\n      <motion.span aria-hidden className=\"font-mono font-medium text-fg tabular-nums\" style={{ fontSize: Math.max(9, Math.round(size * 0.24)) }}>\n        {label}\n      </motion.span>\n    </div>\n  )\n}\n\nconst ARTICLE = [\n  \"Lumen 3.0 closes the books on its own. Every invoice you send is watched from the moment it leaves: opened, viewed, paid or overdue, with the next step already queued.\",\n  \"Reminders go out on the schedule you set, in your voice, and stop the moment money lands. Customers on Lumen pay nine days sooner on average, and nobody on your team writes a chasing email again.\",\n  \"Payments reconcile themselves. Lumen matches bank lines to invoices by amount, reference and timing, flags the rare miss, and posts the rest to the ledger while you sleep.\",\n  \"At month end there is nothing to close. The numbers are already current, the reports are already drafted, and the board pack is waiting in your inbox on the first Monday.\",\n  \"Every change is kept: who made it, when, and why. Auditors get a read-only seat, and your accountant gets the same live view you have.\",\n  \"Lumen Pro is $24 a month per seat, billed yearly, with a 30-day trial that needs no card. Northwind, Acme and 4,000 other teams run their invoicing on it today.\",\n]\n\nfunction ScrollProgress({ variant = \"both\", container, fixed = false, height = 3, size = 40, children, title = \"What is new in Lumen 3.0\", subtitle = \"Release notes · 4 min read\", className }: ScrollProgressProps) {\n  const reduce = useReducedMotion()\n  const panel = React.useRef<HTMLDivElement>(null)\n  const own = !fixed && !container\n  const { scrollYProgress } = useScroll({ container: own ? panel : container })\n  const sprung = useSpring(scrollYProgress, SPRING)\n  const value = reduce ? scrollYProgress : sprung\n  const [percent, setPercent] = React.useState(0)\n  useMotionValueEvent(scrollYProgress, \"change\", (v) => setPercent(Math.round(Math.min(1, Math.max(0, v)) * 100)))\n  const showBar = variant !== \"circle\"\n  const showRing = variant !== \"bar\"\n\n  if (!own) {\n    return (\n      <div data-slot=\"scroll-progress\" className={cn(\"pointer-events-none z-50\", fixed ? \"fixed inset-0\" : \"absolute inset-0\", className)}>\n        {showBar ? <Bar value={value} height={height} percent={showRing ? undefined : percent} className=\"absolute inset-x-0 top-0\" /> : null}\n        {showRing ? <Ring value={value} size={size} percent={percent} className={cn(\"absolute\", fixed ? \"right-4 bottom-4\" : \"top-3 right-3\")} /> : null}\n      </div>\n    )\n  }\n\n  return (\n    <div data-slot=\"scroll-progress\" className={cn(\"relative mx-auto w-full max-w-2xl overflow-hidden rounded-2xl border border-border bg-surface shadow-raised\", className)}>\n      <div className=\"flex h-14 items-center justify-between border-b border-border px-5\">\n        <div className=\"min-w-0\">\n          <p className=\"truncate text-sm font-medium text-fg\">{title}</p>\n          {subtitle ? <p className=\"truncate text-[11px] text-fg-subtle\">{subtitle}</p> : null}\n        </div>\n        {showRing ? <Ring value={value} size={size} percent={percent} /> : null}\n      </div>\n      <div className=\"relative\">\n        {showBar ? <Bar value={value} height={height} percent={showRing ? undefined : percent} className=\"absolute inset-x-0 top-0 z-10\" /> : null}\n        <div ref={panel} className=\"h-72 overflow-y-auto overscroll-contain px-5 py-5 outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset sm:px-6\" tabIndex={0} aria-label=\"Release notes\">\n          {children ?? (\n            <div className=\"max-w-prose space-y-4\">\n              {ARTICLE.map((p, i) => (\n                <p key={i} className=\"text-sm leading-[1.7] text-pretty text-fg-muted first:text-fg\">\n                  {p}\n                </p>\n              ))}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport { ScrollProgress }\nexport type { ScrollProgressProps }\n"
    }
  ]
}
