{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress",
  "type": "registry:ui",
  "title": "Progress",
  "description": "An accessible React progress bar and ring for Next.js and Tailwind CSS that show how far along a task is, with an indeterminate state.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/progress.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { duration, ease } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  animate,\n  motion,\n  type Transition,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\nimport { type ComponentProps, useEffect, useRef } from \"react\";\n\n/*\n * Progress: how far along a task is, as a bar (Progress) or a ring (ProgressRing), both on Radix Progress\n * (role=\"progressbar\" with the value, and no value while it's indeterminate). Owner's roadmap, 2026-10-16.\n * Give each a name with aria-label or aria-labelledby.\n * Motion (.agents/rules/motion.md):\n * - A new value: the bar's fill slides to it (translateX) and the ring's arc draws to it (pathLength). It rises\n *   continuously, not in parts (owner, 2026-10-21): each change takes as long as the gap since the one before,\n *   at an even pace, so updates that keep coming join into one smooth rise and the fill never stops to wait.\n *   A change after a pause eases in on its own (duration.slow, ease.out). Never a spring: its overshoot would\n *   show more than was done, and past 100 at the end. The first paint shows the value as it is.\n * - Indeterminate (`value` null or left out): a segment sweeps across the bar; an arc turns round the ring.\n *   Both are transform loops, which motion hands to the Web Animations API (as Spinner does), so they keep\n *   moving while the page is busy. With reduced motion they stop moving and pulse instead, like Spinner.\n * - ProgressValue, the percentage, counts through every whole number in step with the fill (12, 13, 14…), on\n *   the same timing, so the two never disagree. A deliberate exception to \"never a tween through every\n *   number\": here the number is the bar read aloud.\n */\n\n/** Radix's rules: a max above 0 (else 100), a value from 0 to max (clamped here rather than refused) */\nfunction percentOf(value: number | null | undefined, max: number | undefined) {\n  if (value == null || Number.isNaN(value)) return null;\n  const top = max !== undefined && max > 0 ? max : 100;\n  return (Math.min(Math.max(value, 0), top) / top) * 100;\n}\n\n/** Updates closer together than this join into one continuous rise; further apart, each eases in on its own */\nconst STEADY = duration.slower * 2;\n\n/**\n * How a change to `target` moves: as long as the gap since the last change, at an even pace, while the changes\n * keep coming; else a quick ease. Progress, its ring and ProgressValue see the same changes at the same moments,\n * so they keep in step.\n */\nfunction useFollow(target: number | null, reduce: boolean): Transition {\n  const last = useRef<{ target: number | null; at: number; transition: Transition }>({\n    target,\n    at: Number.NEGATIVE_INFINITY,\n    transition: { duration: 0 },\n  });\n  if (target !== last.current.target) {\n    const now = typeof performance === \"undefined\" ? 0 : performance.now();\n    const gap = (now - last.current.at) / 1000;\n    const transition: Transition = reduce\n      ? { duration: 0 }\n      : gap <= STEADY\n        ? { duration: gap, ease: \"linear\" }\n        : { duration: duration.slow, ease: ease.out };\n    last.current = { target, at: now, transition };\n  }\n  return last.current.transition;\n}\n\n/** The indeterminate loops: a calm sweep, and Spinner's pulse with reduced motion */\nconst sweep = { duration: duration.slower * 3, ease: ease.inOut, repeat: Number.POSITIVE_INFINITY };\nconst turn = { duration: duration.slower * 3, ease: \"linear\", repeat: Number.POSITIVE_INFINITY } as const;\nconst pulse = { duration: duration.slower * 3, ease: ease.inOut, repeat: Number.POSITIVE_INFINITY };\n\nexport type ProgressProps = ComponentProps<typeof ProgressPrimitive.Root>;\n\n/**\n * A bar, built on Radix Progress. `value` from 0 to `max` (100); leave it out, or pass null, while there's\n * no telling how long the task will take.\n */\nfunction Progress({ className, value, max, ...props }: ProgressProps) {\n  const percent = percentOf(value, max);\n  const top = max !== undefined && max > 0 ? max : 100;\n  const reduce = useReducedMotion() ?? false;\n  const follow = useFollow(percent, reduce);\n\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      value={percent === null ? null : (percent / 100) * top}\n      max={top}\n      className={cn(\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\", className)}\n      {...props}\n    >\n      {percent === null ? (\n        <ProgressPrimitive.Indicator asChild>\n          {/* A segment 40% of the track wide, from just off the start to just off the end (250% of itself) */}\n          <motion.div\n            key={reduce ? \"pulse\" : \"sweep\"}\n            data-slot=\"progress-indicator\"\n            className={cn(\n              \"absolute inset-y-0 w-2/5 rounded-full bg-primary\",\n              reduce ? \"left-1/2 -translate-x-1/2\" : \"left-0\",\n            )}\n            animate={\n              reduce ? { opacity: [1, 0.4, 1] } : { transform: [\"translateX(-100%)\", \"translateX(250%)\"] }\n            }\n            transition={reduce ? pulse : sweep}\n          />\n        </ProgressPrimitive.Indicator>\n      ) : (\n        <ProgressPrimitive.Indicator asChild>\n          <motion.div\n            data-slot=\"progress-indicator\"\n            className=\"size-full rounded-full bg-primary\"\n            initial={false}\n            animate={{ x: `-${100 - percent}%` }}\n            transition={follow}\n          />\n        </ProgressPrimitive.Indicator>\n      )}\n    </ProgressPrimitive.Root>\n  );\n}\n\nconst progressRingVariants = cva(\"relative inline-flex shrink-0 items-center justify-center text-primary\", {\n  variants: {\n    size: {\n      sm: \"size-(--size-icon-lg)\",\n      md: \"size-(--size-control-lg)\",\n      lg: \"size-16 text-sm\",\n    },\n  },\n  defaultVariants: { size: \"md\" },\n});\n\n/** Stroke widths on the 24-unit drawing, so the line is about 2.5, 3.3 and 4px at 20, 40 and 64px */\nconst strokes = { sm: 3, md: 2, lg: 1.5 } as const;\n\nexport type ProgressRingProps = Omit<ComponentProps<typeof ProgressPrimitive.Root>, \"children\"> &\n  VariantProps<typeof progressRingVariants> & {\n    /** Shows the percentage in the middle, rolling as it changes (md and lg) */\n    showValue?: boolean;\n  };\n\n/**\n * A ring, built on Radix Progress: the same `value` and `max` as Progress, drawn as an arc that goes round\n * from the top. It takes the text colour (text-primary unless you set another).\n */\nfunction ProgressRing({ className, value, max, size, showValue = false, ...props }: ProgressRingProps) {\n  const percent = percentOf(value, max);\n  const top = max !== undefined && max > 0 ? max : 100;\n  const reduce = useReducedMotion() ?? false;\n  const ring = size ?? \"md\";\n  const stroke = strokes[ring];\n  const radius = 12 - stroke / 2;\n  const indeterminate = percent === null;\n  const loop = !indeterminate ? \"still\" : reduce ? \"pulse\" : \"turn\";\n  const follow = useFollow(percent, reduce);\n\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress-ring\"\n      data-size={ring}\n      value={percent === null ? null : (percent / 100) * top}\n      max={top}\n      className={cn(progressRingVariants({ size }), className)}\n      {...props}\n    >\n      <ProgressPrimitive.Indicator asChild>\n        {/* The drawing turns inside the still root, so nothing passed to the ring turns with it */}\n        <motion.span\n          key={loop}\n          data-slot=\"progress-indicator\"\n          className=\"absolute inset-0 inline-flex\"\n          animate={\n            loop === \"turn\"\n              ? { transform: [\"rotate(0turn)\", \"rotate(1turn)\"] }\n              : loop === \"pulse\"\n                ? { opacity: [1, 0.4, 1] }\n                : undefined\n          }\n          transition={loop === \"turn\" ? turn : loop === \"pulse\" ? pulse : undefined}\n        >\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden className=\"size-full\">\n            <circle cx=\"12\" cy=\"12\" r={radius} stroke=\"currentColor\" strokeWidth={stroke} opacity=\"0.2\" />\n            {/* Starts at the top: the rotation is on a group, since a CSS transform would replace it */}\n            <g transform=\"rotate(-90 12 12)\">\n              <motion.circle\n                cx=\"12\"\n                cy=\"12\"\n                r={radius}\n                stroke=\"currentColor\"\n                strokeWidth={stroke}\n                strokeLinecap=\"round\"\n                initial={false}\n                // At 0 the round cap would still show a dot, so the arc fades out with its length\n                animate={{\n                  pathLength: indeterminate ? 0.25 : percent / 100,\n                  opacity: indeterminate || percent > 0 ? 1 : 0,\n                }}\n                transition={follow}\n              />\n            </g>\n          </svg>\n        </motion.span>\n      </ProgressPrimitive.Indicator>\n      {showValue && percent !== null && ring !== \"sm\" ? (\n        // Positioned, so it paints above the drawing rather than under it\n        <ProgressValue value={percent} className={cn(\"relative\", ring === \"md\" && \"text-xs\")} />\n      ) : null}\n    </ProgressPrimitive.Root>\n  );\n}\n\nexport type ProgressValueProps = Omit<ComponentProps<\"span\">, \"children\"> & {\n  /** The same value and max as the Progress it labels */\n  value?: number | null;\n  max?: number;\n};\n\n/**\n * The percentage as text, for beside a bar or inside a ring. It counts to a new value in step with the fill.\n * Hidden from screen readers: the progressbar already says its value.\n */\nfunction ProgressValue({ value, max, className, ...props }: ProgressValueProps) {\n  const percent = percentOf(value, max);\n  const reduce = useReducedMotion() ?? false;\n  const follow = useFollow(percent, reduce);\n  const count = useMotionValue(percent ?? 0);\n  const shown = useTransform(count, (current) => Math.round(current));\n\n  useEffect(() => {\n    if (percent === null) return;\n    const counting = animate(count, percent, follow);\n    return () => counting.stop();\n  }, [percent, follow, count]);\n\n  if (percent === null) return null;\n  return (\n    <span\n      aria-hidden\n      data-slot=\"progress-value\"\n      className={cn(\"inline-flex font-medium tabular-nums\", className)}\n      {...props}\n    >\n      <motion.span>{shown}</motion.span>\n      <span>%</span>\n    </span>\n  );\n}\n\nexport { Progress, ProgressRing, ProgressValue, progressRingVariants };\n"
    }
  ],
  "categories": [
    "feedback"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/progress"
}
