{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-progress",
  "type": "registry:component",
  "title": "Liquid Progress",
  "description": "A progress bar filled with liquid: each jump surges forward and the rounded front wobbles before it settles, and when there's no telling how long it'll take, a slug of it sloshes end to end.",
  "categories": [
    "component",
    "Liquid"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/liquid-progress.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  motion,\n  useAnimationFrame,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  useVelocity,\n} from \"motion/react\";\nimport { useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TONES = {\n  primary: \"[--liquid:var(--color-primary)] [--liquid-ink:var(--color-primary-foreground)]\",\n  info: \"[--liquid:var(--color-info)] [--liquid-ink:var(--color-white)]\",\n  success: \"[--liquid:var(--color-success)] [--liquid-ink:var(--color-white)]\",\n} as const;\n\nconst HEIGHTS = { sm: 6, default: 10, lg: 28 } as const;\n\nexport interface LiquidProgressProps {\n  value: number | null;\n  size?: keyof typeof HEIGHTS;\n  tone?: keyof typeof TONES;\n  showValue?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\nfunction body(front: number, height: number, amp: number, t: number) {\n  if (front <= 0.5) return \"\";\n  const r = height / 2;\n  const cx = front - r;\n  let d = `M -2 -2 L ${cx.toFixed(2)} -2`;\n  for (let i = 0; i <= 16; i++) {\n    const theta = -Math.PI / 2 + (i / 16) * Math.PI;\n    const push = amp * (0.8 + 0.25 * Math.sin(theta * 2 + t));\n    const x = cx + Math.cos(theta) * (r + push);\n    const y = r + Math.sin(theta) * r;\n    d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`;\n  }\n  return `${d} L ${cx.toFixed(2)} ${height + 2} L -2 ${height + 2} Z`;\n}\n\nexport function LiquidProgress({\n  value,\n  size = \"default\",\n  tone = \"primary\",\n  showValue = false,\n  className,\n  \"aria-label\": ariaLabel = \"Progress\",\n}: LiquidProgressProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  const reduce = useReducedMotion();\n  const h = HEIGHTS[size];\n  const indeterminate = value === null;\n  const pct = indeterminate ? 0 : Math.min(Math.max(value, 0), 100);\n  const filterId = `liquid-progress-${useId().replace(/:/g, \"\")}`;\n\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const measure = () => setWidth(el.offsetWidth);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n\n  const front = useSpring(0, { stiffness: 110, damping: 20, mass: 1 });\n  const velocity = useVelocity(front);\n  const t = useMotionValue(0);\n  const placed = useRef(false);\n\n  useLayoutEffect(() => {\n    if (!width) return;\n    const to = (pct / 100) * width;\n    if (!placed.current || reduce) front.jump(to);\n    else front.set(to);\n    placed.current = true;\n  }, [pct, width, reduce, front]);\n\n  useAnimationFrame((time) => {\n    if (reduce || indeterminate || Math.abs(velocity.get()) < 1) return;\n    t.set(time / 1000);\n  });\n\n  const amp = () => {\n    if (reduce || front.get() >= width - 0.5) return 0;\n    return Math.min(Math.abs(velocity.get()) / 40, h * 0.32);\n  };\n  const frontPath = useTransform(() => body(front.get(), h, amp(), t.get() * 9));\n  const inkClip = useTransform(front, (f) => `inset(0 ${Math.max(width - f, 0)}px 0 0 round 0 ${h / 2}px ${h / 2}px 0)`);\n\n  const slugX = useMotionValue(0);\n  const slugW = useMotionValue(0);\n  const dropX = useMotionValue(0);\n  useAnimationFrame((time) => {\n    if (!indeterminate || !width) return;\n    const period = 2.2;\n    const base = width * 0.32;\n    const phase = reduce ? 0.25 : ((time / 1000) % period) / period;\n    const at = (p: number) => (1 - Math.cos(p * Math.PI * 2)) / 2;\n    const swing = Math.sin(phase * Math.PI * 2);\n    const w = base * (1 + 0.45 * Math.abs(swing));\n    const x = at(phase) * (width - w);\n    slugW.set(w);\n    slugX.set(x);\n    const trailing = swing > 0 ? x + h * 0.2 : x + w - h * 1.2;\n    dropX.set(trailing - swing * h * 1.5);\n  });\n\n  const percent = Math.round(pct);\n\n  return (\n    <div\n      aria-busy={indeterminate || undefined}\n      aria-label={ariaLabel}\n      aria-valuemax={100}\n      aria-valuemin={0}\n      aria-valuenow={indeterminate ? undefined : percent}\n      className={cn(\"relative isolate w-full overflow-hidden rounded-full bg-input\", TONES[tone], className)}\n      ref={ref}\n      role=\"progressbar\"\n      style={{ height: h }}\n    >\n      {width > 0 && !indeterminate && (\n        <>\n          <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" preserveAspectRatio=\"none\">\n            <motion.path d={frontPath} fill=\"var(--liquid)\" />\n          </svg>\n          {showValue && size === \"lg\" && (\n            <>\n              <span className=\"absolute inset-0 flex items-center justify-center font-medium text-muted-foreground text-xs tabular-nums\">\n                {percent}%\n              </span>\n              <motion.span\n                aria-hidden\n                className=\"absolute inset-0 flex items-center justify-center font-medium text-(--liquid-ink) text-xs tabular-nums\"\n                style={{ clipPath: inkClip }}\n              >\n                {percent}%\n              </motion.span>\n            </>\n          )}\n        </>\n      )}\n      {width > 0 && indeterminate && (\n        <>\n          <svg aria-hidden className=\"absolute size-0\">\n            <filter colorInterpolationFilters=\"sRGB\" height=\"300%\" id={filterId} width=\"120%\" x=\"-10%\" y=\"-100%\">\n              <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation={Math.max(h / 4, 1.5)} />\n              <feColorMatrix in=\"blur\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9\" />\n            </filter>\n          </svg>\n          <span className=\"absolute inset-0\" style={{ filter: `url(#${filterId})` }}>\n            <motion.span className=\"absolute inset-y-0 left-0 rounded-full bg-(--liquid)\" style={{ x: slugX, width: slugW }} />\n            <motion.span\n              className=\"absolute top-1/2 left-0 rounded-full bg-(--liquid)\"\n              style={{ x: dropX, width: h, height: h, marginTop: -h / 2 }}\n            />\n          </span>\n          <motion.span\n            className={cn(\"absolute inset-y-0 left-0 rounded-full bg-(--liquid)\", reduce && \"animate-pulse\")}\n            style={{ x: slugX, width: slugW }}\n          />\n        </>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}