{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-progress","type":"registry:component","title":"Progress (Motion)","description":"Progress with motion: the bar glides to each value on a spring, runs while the length is unknown, shines when it's done, and can show its percentage on odometer wheels.","dependencies":["motion","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/motion-odometer.json","https://dashral.com/r/motion-reduced.json","https://dashral.com/r/motion-tokens.json","https://dashral.com/r/progress.json"],"files":[{"path":"registry/dashral/motion/progress.tsx","type":"registry:file","target":"components/dashral/motion/progress.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Progress as ProgressPrimitive } from \"radix-ui\"\nimport { animate, motion, useMotionValue } from \"motion/react\"\nimport type { Progress as KitProgress } from \"../progress\"\nimport { useLocale } from \"../lib/locale\"\nimport { cn } from \"../lib/utils\"\n\nimport { Odometer } from \"../lib/motion-odometer\"\nimport { useReducedMotion } from \"../lib/motion-reduced\"\nimport { duration, ease, spring } from \"../lib/motion-tokens\"\n\nexport type ProgressProps = React.ComponentProps<typeof KitProgress> & {\n  /** The percentage at the end of the bar, on odometer wheels. */\n  showValue?: boolean\n}\n\ntype ControlColor = NonNullable<ProgressProps[\"color\"]>\n\n// The track and the bar, as the kit's Progress has them. Every class is written out in full so Tailwind finds it.\nconst trackColors: Record<ControlColor, string> = {\n  primary: \"bg-primary/15\",\n  neutral: \"bg-foreground/15\",\n  info: \"bg-info/15\",\n  success: \"bg-success/15\",\n  warning: \"bg-warning/15\",\n  destructive: \"bg-destructive/15\",\n  highlight: \"bg-highlight/15\",\n}\nconst barColors: Record<ControlColor, string> = {\n  primary: \"bg-primary\",\n  neutral: \"bg-foreground\",\n  info: \"bg-info\",\n  success: \"bg-success\",\n  warning: \"bg-warning\",\n  destructive: \"bg-destructive\",\n  highlight: \"bg-highlight\",\n}\n\n/**\n * The kit's Progress with motion: the same value, max, colors and props. The bar glides to each new value on a spring\n * (filling from the start when it first shows), a value of null runs a highlight back and forth for work of unknown\n * length, and reaching the end sends a shine across the bar once. `showValue` puts the percentage at its end, on\n * odometer wheels. With reduced motion the bar moves at once and nothing shines or runs.\n */\nexport function Progress({ className, classNames, color = \"primary\", value, max = 100, showValue = false, ...props }: ProgressProps) {\n  const reduced = useReducedMotion()\n  const { dir, locale } = useLocale()\n  const known = value !== null && value !== undefined\n  const share = known ? Math.min(1, Math.max(0, value / max)) : 0\n\n  // How full it shows: from empty when it first appears, then gliding to each new value.\n  const fill = useMotionValue(0)\n  const done = known && share >= 1\n  const [shines, setShines] = React.useState(0)\n  React.useEffect(() => {\n    if (reduced) return fill.jump(share)\n    const controls = animate(fill, share, { ...spring.glide, visualDuration: duration.considered * 1.2 })\n    return () => controls.stop()\n  }, [fill, share, reduced])\n  // One shine each time it reaches the end, not on every render there.\n  const [wasDone, setWasDone] = React.useState(done)\n  if (wasDone !== done) {\n    setWasDone(done)\n    if (done) setShines(shines + 1)\n  }\n\n  const percent = React.useMemo(() => new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 }), [locale])\n  const start = dir === \"rtl\" ? \"right\" : \"left\"\n\n  const bar = (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      className={cn(\"relative h-2 w-full overflow-hidden rounded-full\", trackColors[color], className)}\n      value={value}\n      max={max}\n      {...props}\n    >\n      {/* Keyed apart: the running highlight's position mustn't carry over to the bar when the length becomes known. */}\n      {known ? (\n        <ProgressPrimitive.Indicator key=\"known\" asChild>\n          <motion.div\n            data-slot=\"progress-indicator\"\n            className={cn(\"absolute inset-0 rounded-full\", barColors[color], classNames?.indicator)}\n            style={{ scaleX: fill, transformOrigin: start }}\n          >\n            {shines > 0 && !reduced && (\n              // The shine: a soft band of light crossing the full bar once.\n              <motion.span\n                key={shines}\n                aria-hidden\n                className=\"absolute inset-y-0 w-1/3 bg-linear-to-r from-transparent via-white/60 to-transparent\"\n                initial={{ [start]: \"-35%\" }}\n                animate={{ [start]: \"110%\" }}\n                transition={{ duration: duration.considered * 1.4, ease: ease.standard, delay: duration.considered }}\n              />\n            )}\n          </motion.div>\n        </ProgressPrimitive.Indicator>\n      ) : (\n        <ProgressPrimitive.Indicator key=\"running\" asChild>\n          {/* Unknown length: a highlight running back and forth along the track. */}\n          <motion.div\n            data-slot=\"progress-indicator\"\n            className={cn(\"absolute inset-y-0 w-2/5 rounded-full\", barColors[color], classNames?.indicator)}\n            initial={{ [start]: \"-40%\" }}\n            animate={reduced ? { [start]: \"30%\" } : { [start]: [\"-40%\", \"100%\"] }}\n            transition={reduced ? { duration: 0 } : { duration: 1.4, ease: ease.standard, repeat: Infinity, repeatType: \"loop\" }}\n          />\n        </ProgressPrimitive.Indicator>\n      )}\n    </ProgressPrimitive.Root>\n  )\n\n  if (!showValue) return bar\n  return (\n    <div className=\"flex w-full items-center gap-3\">\n      {bar}\n      <span aria-hidden className=\"min-w-10 text-end text-xs font-medium text-muted-foreground tabular-nums\">\n        {known ? <Odometer value={share} format={percent} className=\"justify-end\" /> : \"…\"}\n      </span>\n    </div>\n  )\n}\n"}],"cssVars":{"theme":{"color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)"},"light":{"info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b"},"dark":{"info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}