{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pour-progress",
  "type": "registry:component",
  "title": "Pour Progress",
  "description": "Progress you can watch fill up: a stream pours in from the top, the surface sloshes and settles as the level eases to the value, and hitting 100% sends out a small ripple. Comes as a round vessel or a slim bar.",
  "categories": [
    "motion",
    "Liquid"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/pour-progress.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type PourProgressVariant = \"round\" | \"bar\";\n\nexport interface PourProgressProps {\n  value: number;\n  max?: number;\n  variant?: PourProgressVariant;\n  size?: number;\n  thickness?: number;\n  label?: string;\n  showValue?: boolean;\n  formatValue?: (percent: number) => string;\n  fillOnMount?: boolean;\n  delay?: number;\n  className?: string;\n}\n\ntype Phase = \"idle\" | \"falling\" | \"pouring\" | \"ending\";\n\ninterface Sim {\n  level: number;\n  vel: number;\n  target: number;\n  slosh: number;\n  ripple: number;\n  t: number;\n  phase: Phase;\n  head: number;\n  vHead: number;\n  tail: number;\n  vTail: number;\n  celebrated: boolean;\n}\n\ninterface Geometry {\n  width: number;\n  height: number;\n  top: number;\n  bottom: number;\n  amp: number;\n  stream: number;\n  spout: number;\n}\n\nconst clamp = (n: number, lo: number, hi: number) => Math.min(Math.max(n, lo), hi);\nconst percentFormat = (p: number) => `${p}%`;\n\nfunction useSafeReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction surfaceOf(g: Geometry, level: number) {\n  return g.bottom + g.amp - level * (g.bottom - g.top + g.amp * 2);\n}\n\nfunction wavePath(g: Geometry, y0: number, tilt: number, ripple: number, t: number, offset: number) {\n  const n = Math.max(10, Math.ceil(g.width / 3));\n  const k1 = (Math.PI * 2) / Math.max(g.width * 0.9, 26);\n  const k2 = (Math.PI * 2) / Math.max(g.width * 0.45, 14);\n  let d = `M0 ${g.height}`;\n  for (let i = 0; i <= n; i++) {\n    const x = (g.width * i) / n;\n    const u = (x - g.width / 2) / (g.width / 2);\n    const y =\n      y0 +\n      tilt * Math.sin((u * Math.PI) / 2) +\n      ripple * (0.6 * Math.sin(k1 * x - t * 5.5 + offset) + 0.4 * Math.sin(k2 * x + t * 3.7 + offset * 1.3));\n    d += ` L${x.toFixed(2)} ${y.toFixed(2)}`;\n  }\n  return `${d} L${g.width} ${g.height} Z`;\n}\n\nfunction streamPath(g: Geometry, from: number, to: number, footed: boolean) {\n  if (to - from < 0.5) return \"\";\n  const x = g.spout;\n  const w = g.stream;\n  const foot = footed ? w * 1.3 : w * 0.38;\n  return [\n    `M${x - w / 2} ${from}`,\n    `L${x + w / 2} ${from}`,\n    `L${x + w * 0.38} ${to - 1.5}`,\n    `Q${x + w * 0.5} ${to + 0.5} ${x + foot} ${to + 1.5}`,\n    `L${x - foot} ${to + 1.5}`,\n    `Q${x - w * 0.5} ${to + 0.5} ${x - w * 0.38} ${to - 1.5}`,\n    \"Z\",\n  ].join(\" \");\n}\n\nexport function PourProgress({\n  value,\n  max = 100,\n  variant = \"round\",\n  size,\n  thickness = 28,\n  label = \"Progress\",\n  showValue,\n  formatValue = percentFormat,\n  fillOnMount = true,\n  delay = 0,\n  className,\n}: PourProgressProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useSafeReducedMotion();\n  const round = variant === \"round\";\n  const height = size ?? 120;\n  const width = round ? height : thickness;\n  const inset = round ? Math.max(3, height * 0.045) : Math.max(2.5, width * 0.12);\n  const geometry: Geometry = {\n    width,\n    height,\n    top: inset,\n    bottom: height - inset,\n    amp: round ? height * 0.034 : Math.min(width * 0.16, 4),\n    stream: round ? Math.max(2.5, height * 0.026) : Math.max(2, width * 0.13),\n    spout: round ? width * 0.63 : width / 2,\n  };\n  const geo = useRef(geometry);\n  geo.current = geometry;\n\n  const target = clamp(max > 0 ? value / max : 0, 0, 1);\n  const initial = fillOnMount ? 0 : target;\n  const showLabel = showValue ?? round;\n  const format = useRef(formatValue);\n  format.current = formatValue;\n\n  const sim = useRef<Sim>({\n    level: initial,\n    vel: 0,\n    target: initial,\n    slosh: 0,\n    ripple: 0,\n    t: 0,\n    phase: \"idle\",\n    head: 0,\n    vHead: 0,\n    tail: 0,\n    vTail: 0,\n    celebrated: initial >= 1,\n  });\n  const front = useRef<SVGPathElement>(null);\n  const back = useRef<SVGPathElement>(null);\n  const clip = useRef<SVGPathElement>(null);\n  const stream = useRef<SVGPathElement>(null);\n  const labels = useRef<(SVGTextElement | null)[]>([]);\n  const raf = useRef(0);\n  const started = useRef(false);\n  const startAt = useRef<number | null>(null);\n  const [celebrations, setCelebrations] = useState(0);\n\n  const draw = () => {\n    const s = sim.current;\n    const g = geo.current;\n    const surface = surfaceOf(g, s.level);\n    const sway = s.slosh * Math.cos(s.t * Math.PI * 2.6);\n    const d = wavePath(g, surface, sway, s.ripple, s.t, 0);\n    front.current?.setAttribute(\"d\", d);\n    clip.current?.setAttribute(\"d\", d);\n    back.current?.setAttribute(\"d\", wavePath(g, surface - s.ripple * 0.35, -sway * 0.8, s.ripple * 1.1, s.t, 1.9));\n    const pourTop = g.top - 4;\n    const streamD =\n      s.phase === \"idle\"\n        ? \"\"\n        : s.phase === \"falling\"\n          ? streamPath(g, pourTop, s.head, false)\n          : streamPath(g, s.phase === \"ending\" ? s.tail : pourTop, surface, true);\n    stream.current?.setAttribute(\"d\", streamD);\n    const shown = Math.abs(s.target - s.level) < 0.004 ? s.target : s.level;\n    const text = format.current(Math.round(shown * 100));\n    for (const el of labels.current) if (el && el.textContent !== text) el.textContent = text;\n  };\n\n  const step = (dt: number) => {\n    const s = sim.current;\n    const g = geo.current;\n    const gravity = g.height * 22;\n    const pourTop = g.top - 4;\n    s.t += dt;\n    if (s.phase === \"idle\" && s.target > s.level + 0.002) {\n      s.phase = \"falling\";\n      s.head = pourTop;\n      s.vHead = g.height * 1.5;\n    }\n    if (s.phase === \"ending\" && s.target > s.level + 0.006) s.phase = \"pouring\";\n    if (s.phase === \"falling\") {\n      s.vHead += gravity * dt;\n      s.head += s.vHead * dt;\n      if (s.head >= surfaceOf(g, s.level)) {\n        s.phase = \"pouring\";\n        s.slosh = Math.max(s.slosh, g.amp * 0.6);\n        s.ripple = Math.max(s.ripple, g.amp * 0.6);\n      }\n    }\n    if (s.phase === \"pouring\" || s.phase === \"ending\" || s.target < s.level) {\n      const w = 5;\n      s.vel += (w * w * (s.target - s.level) - 2 * w * s.vel) * dt;\n      s.level += s.vel * dt;\n      s.slosh = Math.max(s.slosh, Math.min(g.amp, Math.abs(s.vel) * g.amp * 1.6));\n    }\n    if (s.phase === \"pouring\") {\n      s.ripple = Math.max(s.ripple, g.amp * 0.4);\n      if (s.target - s.level < 0.004) {\n        s.phase = \"ending\";\n        s.tail = pourTop;\n        s.vTail = g.height * 0.5;\n      }\n    }\n    if (s.phase === \"ending\") {\n      s.vTail += gravity * dt;\n      s.tail += s.vTail * dt;\n      if (s.tail >= surfaceOf(g, s.level)) s.phase = \"idle\";\n    }\n    s.slosh *= Math.exp(-dt * 1.9);\n    s.ripple *= Math.exp(-dt * 2.6);\n    if (s.target < 0.999) s.celebrated = false;\n    if (s.phase === \"idle\" && s.target >= 0.999 && s.level > 0.99 && !s.celebrated) {\n      s.celebrated = true;\n      s.ripple = g.amp;\n      setCelebrations((c) => c + 1);\n    }\n    const settled =\n      s.phase === \"idle\" &&\n      Math.abs(s.target - s.level) < 0.0005 &&\n      Math.abs(s.vel) < 0.001 &&\n      s.slosh < 0.04 &&\n      s.ripple < 0.04;\n    if (settled) {\n      s.level = s.target;\n      s.vel = 0;\n      s.slosh = 0;\n      s.ripple = 0;\n    }\n    return !settled;\n  };\n\n  const kick = () => {\n    if (raf.current) return;\n    let last = performance.now();\n    const tick = (now: number) => {\n      const dt = Math.min((now - last) / 1000, 1 / 30);\n      last = now;\n      const busy = step(dt);\n      draw();\n      raf.current = busy ? requestAnimationFrame(tick) : 0;\n    };\n    raf.current = requestAnimationFrame(tick);\n  };\n\n  useEffect(() => {\n    const s = sim.current;\n    if (reduce) {\n      cancelAnimationFrame(raf.current);\n      raf.current = 0;\n      const wasFull = s.level >= 0.999;\n      Object.assign(s, { target, level: target, vel: 0, phase: \"idle\", slosh: 0, ripple: 0 });\n      draw();\n      if (target >= 0.999 && !wasFull && started.current) setCelebrations((c) => c + 1);\n      started.current = true;\n      return;\n    }\n    const apply = () => {\n      s.target = target;\n      if (target < s.level - 0.001) s.slosh = Math.max(s.slosh, geo.current.amp * 0.5);\n      kick();\n    };\n    startAt.current ??= performance.now() + delay;\n    const wait = startAt.current - performance.now();\n    if (wait <= 0) {\n      apply();\n      return;\n    }\n    const timer = setTimeout(apply, wait);\n    return () => clearTimeout(timer);\n  }, [target, reduce]);\n\n  useEffect(\n    () => () => {\n      cancelAnimationFrame(raf.current);\n      raf.current = 0;\n    },\n    [],\n  );\n\n  const g = geometry;\n  const flat = wavePath(g, surfaceOf(g, initial), 0, 0, 0, 0);\n  const vesselClip = `pour-vessel-${uid}`;\n  const liquidClip = `pour-liquid-${uid}`;\n  const innerRx = (width - inset * 2) / 2;\n  const initialText = formatValue(Math.round(initial * 100));\n\n  return (\n    <div\n      aria-label={label}\n      aria-valuemax={max}\n      aria-valuemin={0}\n      aria-valuenow={Math.round(clamp(value, 0, max))}\n      aria-valuetext={formatValue(Math.round(target * 100))}\n      className={cn(\"relative inline-block shrink-0 text-sky-600 dark:text-sky-400\", className)}\n      role=\"progressbar\"\n      style={{ width, height }}\n    >\n      {celebrations > 0 && (\n        <motion.span\n          animate={\n            reduce\n              ? { opacity: [0, 0.5, 0] }\n              : { opacity: [0.55, 0], scaleX: [1, round ? 1.22 : 1.6], scaleY: [1, round ? 1.22 : 1.08] }\n          }\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 rounded-full border-2 border-current\"\n          key={celebrations}\n          transition={{ duration: reduce ? 0.6 : 0.7, ease: [0.23, 1, 0.32, 1] }}\n        />\n      )}\n      <svg aria-hidden className=\"block overflow-visible\" height={height} viewBox={`0 0 ${width} ${height}`} width={width}>\n        <defs>\n          <clipPath id={vesselClip}>\n            {round ? (\n              <circle cx={width / 2} cy={height / 2} r={width / 2 - inset} />\n            ) : (\n              <rect height={height - inset * 2} rx={innerRx} width={width - inset * 2} x={inset} y={inset} />\n            )}\n          </clipPath>\n          <clipPath id={liquidClip}>\n            <path d={flat} ref={clip} />\n          </clipPath>\n        </defs>\n        {round ? (\n          <circle className=\"fill-muted stroke-border\" cx={width / 2} cy={height / 2} r={width / 2 - 0.5} />\n        ) : (\n          <rect className=\"fill-muted stroke-border\" height={height - 1} rx={width / 2 - 0.5} width={width - 1} x={0.5} y={0.5} />\n        )}\n        <g clipPath={`url(#${vesselClip})`}>\n          <path className=\"fill-current opacity-35\" d={flat} ref={back} />\n          <path className=\"fill-current\" d={flat} ref={front} />\n          <path className=\"fill-current\" d=\"\" ref={stream} />\n        </g>\n        {showLabel && (\n          <>\n            <text\n              className=\"fill-foreground font-semibold tabular-nums\"\n              dominantBaseline=\"central\"\n              ref={(el) => {\n                labels.current[0] = el;\n              }}\n              style={{ fontSize: height * 0.19 }}\n              textAnchor=\"middle\"\n              x={width / 2}\n              y={height / 2}\n            >\n              {initialText}\n            </text>\n            <g clipPath={`url(#${vesselClip})`}>\n              <text\n                className=\"fill-background font-semibold tabular-nums\"\n                clipPath={`url(#${liquidClip})`}\n                dominantBaseline=\"central\"\n                ref={(el) => {\n                  labels.current[1] = el;\n                }}\n                style={{ fontSize: height * 0.19 }}\n                textAnchor=\"middle\"\n                x={width / 2}\n                y={height / 2}\n              >\n                {initialText}\n              </text>\n            </g>\n          </>\n        )}\n      </svg>\n    </div>\n  );\n}\n"
    }
  ]
}