{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress",
  "type": "registry:ui",
  "title": "Progress",
  "description": "Show how much of a known task is complete.",
  "categories": [
    "feedback"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/progress/progress.tsx",
      "type": "registry:component",
      "target": "@components/xzero/progress/progress.tsx",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform, type HTMLMotionProps, type TargetAndTransition, type Transition } from \"motion/react\";\nimport { Check } from \"lucide-react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./progress.module.css\";\n\nexport interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  value?: number;\n  max?: number;\n  label?: string;\n  showValue?: boolean;\n}\n\nconst exitFast: Transition = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };\nconst textIn: TargetAndTransition = { opacity: 0, y: \"0.3em\", filter: `blur(${motionTokens.blur.soft}px)` };\nconst textOut: TargetAndTransition = { opacity: 0, y: \"-0.3em\", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };\nconst iconIn: TargetAndTransition = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };\nconst shown: TargetAndTransition = { opacity: 1, y: \"0em\", scale: 1, filter: \"blur(0px)\" };\nconst fadeOut: TargetAndTransition = { opacity: 0, transition: { duration: motionTokens.duration.instant } };\n\n/** Outgoing copies are hidden from assistive tech while they fade. */\nfunction Swap(props: HTMLMotionProps<\"span\">) {\n  const present = useIsPresent();\n  return <motion.span {...props} aria-hidden={present ? props[\"aria-hidden\"] : true} />;\n}\n\nexport function Progress({ value = 0, max = 100, label, showValue = false, className, ...props }: ProgressProps) {\n  const reduce = useReducedMotion();\n  const safeMax = max > 0 ? max : 100;\n  const safeValue = Math.min(Math.max(value, 0), safeMax);\n  const percentage = Math.round((safeValue / safeMax) * 100);\n  const complete = percentage >= 100;\n  // One spring drives both the fill and the counted label, so the number always matches the bar.\n  const progress = useMotionValue(percentage);\n  // The fill slides in from the left instead of scaling, so its rounded end keeps its shape at every value.\n  const x = useTransform(progress, latest => `${Math.min(Math.max(latest, 0), 100) - 100}%`);\n  const counted = useTransform(progress, latest => `${Math.round(Math.min(Math.max(latest, 0), 100))}%`);\n  useEffect(() => {\n    if (reduce) { progress.jump(percentage); return; }\n    const controls = animate(progress, percentage, motionTokens.spring.smooth);\n    return () => controls.stop();\n  }, [percentage, progress, reduce]);\n  const enter: Transition = reduce ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };\n  const classes = [styles.progress, className].filter(Boolean).join(\" \");\n  return <div {...props} className={classes} data-complete={complete ? \"\" : undefined} role=\"progressbar\" aria-label={label ?? \"Progress\"} aria-valuemin={0} aria-valuemax={safeMax} aria-valuenow={safeValue} aria-valuetext={`${percentage}%`}>\n    {(label || showValue) ? <div className={styles.meta}>\n      {label ? <span className={styles.label}><AnimatePresence mode=\"popLayout\" initial={false}><Swap key={label} className={styles.line} initial={reduce ? { opacity: 0 } : textIn} animate={shown} exit={reduce ? fadeOut : textOut} transition={enter}>{label}</Swap></AnimatePresence></span> : <span />}\n      {showValue ? <span className={styles.value}>\n        {/* Completion lands as the fill arrives: a check settles in beside the final count. */}\n        <AnimatePresence initial={false}>{complete && <motion.span key=\"done\" className={styles.done} initial={reduce ? { opacity: 0 } : iconIn} animate={shown} exit={reduce ? fadeOut : { ...iconIn, transition: exitFast }} transition={reduce ? enter : { ...motionTokens.spring.snappy, delay: .24 }}><Check size={14} strokeWidth={1.75} aria-hidden=\"true\" /></motion.span>}</AnimatePresence>\n        <motion.span className={styles.count}>{counted}</motion.span>\n      </span> : null}\n    </div> : null}\n    <div className={styles.track}><motion.span className={styles.fill} style={{ x }} /></div>\n  </div>;\n}\n\nexport default Progress;\n"
    },
    {
      "path": "registry/components/progress/progress.module.css",
      "type": "registry:component",
      "target": "@components/xzero/progress/progress.module.css",
      "content": ".progress { width: 100%; color: var(--text-secondary); font-size: var(--text-sm); } .meta { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px; line-height: var(--leading-body); } .label { position: relative; min-width: 0; } .line { display: block; } .value { display: inline-flex; flex: none; align-items: center; gap: 4px; color: var(--text-muted); font-variant-numeric: tabular-nums; } .count { display: inline-grid; justify-items: end; } /* A hidden \"100%\" row reserves the widest count, so the check never shifts as the number gains a digit. */ .count::before { content: \"100%\"; height: 0; overflow: hidden; visibility: hidden; } .done { display: grid; place-items: center; color: var(--success); } .track { height: 7px; overflow: hidden; border: 1px solid var(--border); border-radius: 99px; background: var(--surface-muted); } .fill { display: block; width: 100%; height: 100%; border-radius: inherit; background: var(--accent); transition: background-color var(--duration-standard) var(--ease-standard); } .progress[data-complete] .fill { background: var(--success); transition-delay: 200ms; } @media (prefers-reduced-motion: reduce) { .fill, .progress[data-complete] .fill { transition: none; } }\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/progress",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "status",
      "loading"
    ],
    "docs": "https://ui.xzero.co.uk/components/progress",
    "markdown": "https://ui.xzero.co.uk/components/progress/markdown"
  }
}