{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rich-progress",
  "title": "Rich Progress",
  "description": "A sharp, glossy progress bar with a springy square knob you can drag.",
  "dependencies": [
    "motion",
    "class-variance-authority"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/rich-progress.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nconst richProgressVariants = cva(\"flex w-full max-w-xl flex-col gap-1.5 select-none\", {\n  variants: {\n    color: {\n      default: \"text-foreground\",\n      blue: \"text-blue-500\",\n      purple: \"text-purple-500\",\n      pink: \"text-pink-500\",\n      red: \"text-red-600\",\n      orange: \"text-orange-500\",\n      yellow: \"text-yellow-500\",\n      green: \"text-green-500\",\n      teal: \"text-teal-500\",\n      cyan: \"text-cyan-500\",\n      indigo: \"text-indigo-500\",\n      violet: \"text-violet-500\",\n      rose: \"text-rose-500\",\n      amber: \"text-amber-500\",\n      lime: \"text-lime-500\",\n      sky: \"text-sky-500\",\n      emerald: \"text-emerald-500\",\n    },\n  },\n  defaultVariants: {\n    color: \"default\",\n  },\n});\n\nconst knobVariants = cva(\n  [\n    \"pointer-events-none absolute top-1/2 -translate-x-1/2 -translate-y-1/2\",\n    \"bg-background dark:bg-foreground ring-border ring-1\",\n  ],\n  {\n    variants: {\n      knob: {\n        circle: \"rounded-full\",\n        square: \"rounded-none\",\n      },\n      size: {\n        sm: \"size-3.5\",\n        default: \"size-4.5\",\n        lg: \"size-5\",\n      },\n    },\n    defaultVariants: {\n      knob: \"square\",\n      size: \"default\",\n    },\n  },\n);\n\nconst trackHeights = { sm: \"h-1\", default: \"h-1.5\", lg: \"h-2\" } as const;\n\nexport type RichProgressColor = NonNullable<VariantProps<typeof richProgressVariants>[\"color\"]>;\n\nexport type RichProgressSize = keyof typeof trackHeights;\n\nexport type RichProgressKnob = NonNullable<VariantProps<typeof knobVariants>[\"knob\"]>;\n\nexport interface RichProgressProps {\n  value: number;\n  onChange?: (value: number) => void;\n  label?: string;\n  color?: RichProgressColor;\n  size?: RichProgressSize;\n  showValue?: boolean;\n  valueLabel?: string;\n  knob?: RichProgressKnob | boolean;\n  className?: string;\n  trackClassName?: string;\n  fillClassName?: string;\n}\n\nconst spring = { stiffness: 520, damping: 42, mass: 0.5 };\n\nconst RichProgress = ({\n  value,\n  onChange,\n  label,\n  color = \"default\",\n  size = \"default\",\n  showValue = true,\n  valueLabel,\n  knob = \"square\",\n  className,\n  trackClassName,\n  fillClassName,\n}: RichProgressProps) => {\n  const reduceMotion = useReducedMotion();\n  const clamped = Math.min(100, Math.max(0, value));\n  const knobShape: RichProgressKnob | null =\n    knob === true ? \"square\" : knob === false ? null : knob;\n\n  const trackRef = React.useRef<HTMLDivElement>(null);\n  const draggingRef = React.useRef(false);\n  const [dragging, setDragging] = React.useState(false);\n\n  const springValue = useSpring(clamped, spring);\n\n  React.useEffect(() => {\n    if (reduceMotion) springValue.jump(clamped);\n    else springValue.set(clamped);\n  }, [clamped, reduceMotion, springValue]);\n\n  const width = useTransform(springValue, (v) => `${v}%`);\n\n  const updateFromPointer = (clientX: number) => {\n    const track = trackRef.current;\n    if (!track || !onChange) return;\n    const rect = track.getBoundingClientRect();\n    if (rect.width === 0) return;\n    const percentage = ((clientX - rect.left) / rect.width) * 100;\n    onChange(Math.round(Math.min(100, Math.max(0, percentage))));\n  };\n\n  const endDrag = (event: React.PointerEvent<HTMLDivElement>) => {\n    draggingRef.current = false;\n    setDragging(false);\n    if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n      event.currentTarget.releasePointerCapture(event.pointerId);\n    }\n  };\n\n  const formattedValue = clamped.toFixed(2).replace(/\\.?0+$/, \"\") || \"0\";\n  const motionTransition = reduceMotion ? { duration: 0 } : { type: \"spring\" as const, ...spring };\n\n  return (\n    <div className={cn(richProgressVariants({ color }), className)}>\n      {(label || showValue) && (\n        <div className=\"flex items-center justify-between gap-3\">\n          {label && <span className=\"text-foreground text-sm font-medium\">{label}</span>}\n          {showValue && (\n            <span\n              className={cn(\n                \"text-muted-foreground ml-auto tabular-nums\",\n                size === \"sm\" ? \"text-xs\" : \"text-sm\",\n              )}\n            >\n              {valueLabel ?? formattedValue}\n            </span>\n          )}\n        </div>\n      )}\n\n      <div\n        ref={trackRef}\n        role={onChange ? \"slider\" : \"progressbar\"}\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={Math.round(clamped)}\n        aria-valuetext={valueLabel}\n        aria-label={label}\n        tabIndex={onChange ? 0 : undefined}\n        onPointerDown={(event) => {\n          if (!onChange || event.button !== 0) return;\n          draggingRef.current = true;\n          setDragging(true);\n          event.currentTarget.setPointerCapture(event.pointerId);\n          updateFromPointer(event.clientX);\n        }}\n        onPointerMove={(event) => {\n          if (draggingRef.current) updateFromPointer(event.clientX);\n        }}\n        onPointerUp={endDrag}\n        onPointerCancel={endDrag}\n        onKeyDown={(event) => {\n          if (!onChange) return;\n          const moves: Record<string, number> = {\n            ArrowRight: clamped + 1,\n            ArrowUp: clamped + 1,\n            ArrowLeft: clamped - 1,\n            ArrowDown: clamped - 1,\n            PageUp: clamped + 10,\n            PageDown: clamped - 10,\n            Home: 0,\n            End: 100,\n          };\n          if (!(event.key in moves)) return;\n          event.preventDefault();\n          onChange(Math.min(100, Math.max(0, moves[event.key])));\n        }}\n        className={cn(\n          \"relative w-full touch-none py-2 outline-none\",\n          onChange && \"cursor-pointer\",\n          \"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2\",\n        )}\n      >\n        <div className={cn(\"bg-foreground/10 relative w-full\", trackHeights[size], trackClassName)}>\n          <motion.div\n            className={cn(\n              \"relative h-full overflow-hidden bg-current shadow-[inset_0_-1px_0_rgb(0_0_0/0.25)]\",\n              fillClassName,\n            )}\n            style={{ width }}\n          >\n            <span\n              aria-hidden\n              className=\"absolute inset-x-0 top-0 h-1/2 bg-gradient-to-b from-white/45 to-white/0\"\n            />\n          </motion.div>\n\n          {knobShape && (\n            <motion.div\n              aria-hidden\n              className={knobVariants({ knob: knobShape, size })}\n              style={{ left: width }}\n              animate={{ scale: dragging ? 1.2 : 1 }}\n              transition={motionTransition}\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nRichProgress.displayName = \"RichProgress\";\n\nexport { RichProgress, richProgressVariants, knobVariants };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}