{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress-ring",
  "type": "registry:ui",
  "title": "Progress Ring",
  "description": "A circular progress ring whose value springs to its target, so a number that changes mid travel keeps the speed it already had.",
  "registryDependencies": [
    "utils",
    "use-spring"
  ],
  "files": [
    {
      "path": "registry/loomui/progress-ring.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useSpring, type SpringOptions } from \"@/registry/lib/use-spring\"\n\nexport interface ProgressRingProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\"\n> {\n  /** Where the ring is now. */\n  value: number\n  /** What a full ring means. */\n  max?: number\n  /** Outside diameter in pixels. */\n  size?: number\n  /** Stroke width in pixels. */\n  thickness?: number\n  /** How the value travels when it changes. */\n  spring?: SpringOptions\n  /** Print the number in the middle. */\n  showValue?: boolean\n  /** Turns the value into the text in the middle. */\n  format?: (value: number) => string\n  /** Accessible name. Give it one if the ring is the only label. */\n  label?: string\n  /** Render the ring at its value without animating to it. */\n  disabled?: boolean\n  /** Children sit in the middle instead of the number. */\n  center?: React.ReactNode\n}\n\n/** The box the ring is drawn in. Every measurement is a share of this. */\nconst BOX = 100\n\nconst clamp = (value: number, max: number) =>\n  Math.min(Math.max(value, 0), max) / (max || 1)\n\n/**\n * A ring that fills to a value, on a spring.\n *\n * The arc is drawn by walking the stroke's own dash offset around the circle.\n * That is a paint rather than a composite, which is the one place this steps\n * outside transform and opacity, and there is no way to draw an arc without it.\n * It stays cheap because only the stroke is repainted, never the layout.\n *\n * The value springs rather than tweens, so a target that changes while the ring\n * is still moving keeps the speed it already had. A progress ring that restarts\n * from a standstill every time the number updates is the tell.\n */\nexport function ProgressRing({\n  value,\n  max = 100,\n  size = 120,\n  thickness = 10,\n  spring,\n  showValue = true,\n  format,\n  label,\n  disabled = false,\n  center,\n  className,\n  style,\n  ...props\n}: ProgressRingProps) {\n  const arcRef = React.useRef<SVGCircleElement>(null)\n  const textRef = React.useRef<HTMLSpanElement>(null)\n\n  const radius = (BOX - thickness) / 2\n  const circumference = 2 * Math.PI * radius\n  const target = clamp(value, max)\n\n  const paint = React.useCallback(\n    ({ fill = 0 }: Record<string, number>) => {\n      const arc = arcRef.current\n      if (arc) {\n        arc.style.strokeDashoffset = `${circumference * (1 - fill)}`\n      }\n\n      const text = textRef.current\n      if (text) {\n        const shown = fill * max\n        text.textContent = format ? format(shown) : `${Math.round(shown)}`\n      }\n    },\n    [circumference, format, max]\n  )\n\n  const { to, set } = useSpring(paint, {\n    duration: 0.6,\n    bounce: 0,\n    ...spring,\n  })\n\n  // Written on mount as well, so the first paint is the value rather than an\n  // empty ring that fills itself in front of someone who did not ask.\n  const started = React.useRef(false)\n\n  React.useLayoutEffect(() => {\n    const reduced =\n      typeof window !== \"undefined\" &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n    if (!started.current || disabled || reduced) {\n      started.current = true\n      set({ fill: target })\n      return\n    }\n\n    to({ fill: target })\n  }, [target, disabled, set, to])\n\n  return (\n    <div\n      data-slot=\"progress-ring\"\n      role=\"progressbar\"\n      aria-valuenow={Math.round(value)}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      aria-label={label}\n      className={cn(\n        \"relative inline-grid shrink-0 place-items-center\",\n        className\n      )}\n      style={{ width: size, height: size, ...style }}\n      {...props}\n    >\n      <svg\n        viewBox={`0 0 ${BOX} ${BOX}`}\n        className=\"size-full -rotate-90\"\n        aria-hidden=\"true\"\n      >\n        <circle\n          data-slot=\"progress-ring-track\"\n          cx={BOX / 2}\n          cy={BOX / 2}\n          r={radius}\n          fill=\"none\"\n          strokeWidth={thickness}\n          className=\"stroke-muted-foreground/15\"\n        />\n        <circle\n          ref={arcRef}\n          data-slot=\"progress-ring-arc\"\n          cx={BOX / 2}\n          cy={BOX / 2}\n          r={radius}\n          fill=\"none\"\n          strokeWidth={thickness}\n          strokeLinecap=\"round\"\n          strokeDasharray={circumference}\n          strokeDashoffset={circumference}\n          className=\"stroke-accent\"\n        />\n      </svg>\n\n      {center ?? null}\n\n      {showValue && !center ? (\n        <span\n          ref={textRef}\n          data-slot=\"progress-ring-value\"\n          className=\"absolute font-medium tabular-nums\"\n          style={{ fontSize: size * 0.22 }}\n        />\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}