{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"progress","type":"registry:component","title":"Progress","description":"How far along, as a bar or a ring: the fill eases to each value, the percent rolls, and at 100 it turns green and a check draws itself.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/number-roll.json?as=dependency","https://hairlineui.com/r/text-morph.json?as=dependency","https://hairlineui.com/r/ui-interaction-motion.json?as=dependency"],"files":[{"path":"registry/progress.tsx","type":"registry:component","content":"// Hairline UI · progress · MIT License\n// Copyright (c) 2026 Hairline UI · https://hairlineui.com/license#components\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { NumberRoll } from \"./number-roll\";\nimport { TextMorph } from \"./text-morph\";\nimport { EASE_OUT, useInteractionReducedMotion } from \"./ui-interaction-motion\";\n\n/* ─────────────────────────────────────────────────────────\n * PROGRESS: how far along, as a bar or a ring\n *\n *   advance   the fill eases to each new value (it never jumps a\n *             step) and the percent rolls the way it moved\n *   unknown   without a value, a short segment sweeps along\n *   done      at 100 the fill turns green, a check draws itself\n *             and the label morphs to `doneLabel`\n *   error     the fill turns red where it stopped\n * ───────────────────────────────────────────────────────── */\n\nexport interface ProgressProps {\n  /** 0–100; leave it out for \"working, no idea how long\". */\n  value?: number | null;\n  /** Shown above a bar, beside the percent. */\n  label?: string;\n  /** Replaces the label at 100. */\n  doneLabel?: string;\n  error?: boolean;\n  showValue?: boolean;\n  variant?: \"bar\" | \"ring\";\n  /** Ring size in px. */\n  size?: number;\n  className?: string;\n}\n\nfunction Check({ on, reduced }: { on: boolean; reduced: boolean }) {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-3.5\">\n      <path\n        d=\"M3.5 8.5 6.5 11.5 12.5 4.5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        pathLength={1}\n        strokeDasharray={1}\n        style={{ strokeDashoffset: on ? 0 : 1, transition: reduced ? \"none\" : `stroke-dashoffset 360ms ${EASE_OUT} 160ms` }}\n      />\n    </svg>\n  );\n}\n\nexport function Progress({ value, label, doneLabel = \"Done\", error, showValue = true, variant = \"bar\", size = 44, className = \"\" }: ProgressProps) {\n  const reduced = useInteractionReducedMotion();\n  const known = value !== undefined && value !== null;\n  const v = known ? Math.max(0, Math.min(100, value)) : 0;\n  const done = known && v >= 100 && !error;\n  const tone = error ? \"bg-red-500\" : done ? \"bg-emerald-500\" : \"bg-primary\";\n  const text = error ? \"text-red-600 dark:text-red-400\" : done ? \"text-emerald-600 dark:text-emerald-400\" : \"text-foreground\";\n  const aria = {\n    role: \"progressbar\" as const,\n    \"aria-label\": label ?? \"Progress\",\n    \"aria-valuemin\": 0,\n    \"aria-valuemax\": 100,\n    \"aria-valuenow\": known ? Math.round(v) : undefined,\n    \"aria-busy\": !done || undefined,\n  };\n\n  if (variant === \"ring\") {\n    const r = 15;\n    return (\n      <span {...aria} className={`relative inline-flex shrink-0 items-center justify-center ${className}`} style={{ width: size, height: size }}>\n        <svg viewBox=\"0 0 36 36\" className={`absolute inset-0 size-full -rotate-90 ${known ? \"\" : \"animate-spin [animation-duration:1.1s]\"}`} aria-hidden=\"true\">\n          <circle cx=\"18\" cy=\"18\" r={r} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" className=\"text-muted\" />\n          <circle\n            cx=\"18\"\n            cy=\"18\"\n            r={r}\n            fill=\"none\"\n            strokeWidth=\"3\"\n            strokeLinecap=\"round\"\n            pathLength={100}\n            strokeDasharray=\"100\"\n            className={`${error ? \"stroke-red-500\" : done ? \"stroke-emerald-500\" : \"stroke-primary\"} transition-[stroke] duration-300`}\n            style={{ strokeDashoffset: known ? 100 - v : 72, transition: reduced ? \"none\" : `stroke-dashoffset 600ms ${EASE_OUT}, stroke 300ms` }}\n          />\n        </svg>\n        <span className={`relative flex items-center justify-center text-[11px] font-medium tabular-nums ${text}`}>\n          {done ? <Check on reduced={reduced} /> : known && showValue ? <NumberRoll value={Math.round(v)} suffix=\"%\" /> : null}\n        </span>\n      </span>\n    );\n  }\n\n  return (\n    <div className={`flex w-full flex-col gap-2 ${className}`}>\n      {(label || showValue) && (\n        <div className=\"flex items-baseline justify-between gap-3 text-[13px]\">\n          <span className={`flex items-center gap-1.5 font-medium transition-colors duration-300 ${done ? text : \"text-foreground\"}`}>\n            <span className=\"grid\" style={{ gridTemplateColumns: done ? \"1fr\" : \"0fr\", transition: reduced ? \"none\" : `grid-template-columns 320ms ${EASE_OUT}` }}>\n              <span className=\"min-w-0 overflow-hidden\">\n                <Check on={done} reduced={reduced} />\n              </span>\n            </span>\n            {label && <TextMorph>{done ? doneLabel : label}</TextMorph>}\n          </span>\n          {showValue && known && (\n            <span className=\"tabular-nums text-muted-foreground\">\n              <NumberRoll value={Math.round(v)} suffix=\"%\" />\n            </span>\n          )}\n        </div>\n      )}\n      <div {...aria} className=\"relative h-1.5 w-full overflow-hidden rounded-full bg-muted\">\n        {known ? (\n          <div\n            className={`absolute inset-0 origin-left rounded-full transition-colors duration-300 ${tone}`}\n            style={{ transform: `scaleX(${v / 100})`, transition: reduced ? \"none\" : `transform 600ms ${EASE_OUT}, background-color 300ms` }}\n          />\n        ) : (\n          <div className={`absolute inset-y-0 left-0 w-[30%] rounded-full ${tone} [animation:ui-indeterminate_1.3s_cubic-bezier(0.65,0,0.35,1)_infinite] motion-reduce:[animation-duration:3s]`} />\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default Progress;\n"}],"css":{"@keyframes ui-indeterminate":{"from":{"transform":"translateX(-100%)"},"to":{"transform":"translateX(340%)"}}},"docs":"Docs and live preview: https://hairlineui.com/ui/progress. MIT licensed: https://hairlineui.com/license#components","meta":{"license":"MIT"}}