{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"wavy-progress","type":"registry:ui","title":"Wavy progress","description":"Progress drawn as a travelling wave: the done part ripples while work is happening, the rest is a flat track that ends in a stop mark, and the wave lies flat once the work is finished. As a bar or a ring, with an indeterminate sweep when the amount is unknown.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/wavy-progress.tsx","type":"registry:ui","target":"components/ui/wavy-progress.tsx","content":"\"use client\";\nimport { useId, type ComponentProps, type CSSProperties } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./wavy-progress.css\";\n\nexport type WavyProgressProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  /** Amount done. Leave it out while the amount is unknown. */\n  value?: number;\n  max?: number;\n  /** A bar across its container, or a ring. */\n  variant?: \"linear\" | \"circular\";\n  /** Diameter of the ring in px. */\n  size?: number;\n  /** Names the progress for assistive technology. */\n  label: string;\n  /** Shows the percentage beside the bar or inside the ring. */\n  showValue?: boolean;\n};\n\n// One ring of `waves` crests around radius r, as a closed path.\nfunction ringPath(r: number, amplitude: number, waves: number, c: number) {\n  const steps = waves * 12;\n  let d = \"\";\n  for (let i = 0; i <= steps; i++) {\n    const a = (i / steps) * Math.PI * 2;\n    const rr = r + amplitude * Math.sin(a * waves);\n    d += `${i ? \"L\" : \"M\"}${(c + rr * Math.cos(a)).toFixed(2)} ${(c + rr * Math.sin(a)).toFixed(2)}`;\n  }\n  return `${d}Z`;\n}\n\n/**\n * Progress drawn as a travelling wave: the done part ripples, the rest is a flat track ending in a stop mark. The wave\n * lies flat once the work is done, and under reduced motion it is a plain bar or ring. Without a value it runs as an\n * indeterminate sweep.\n */\nexport function WavyProgress({ value, max = 100, variant = \"linear\", size = 48, label, showValue = false, className, style, ...props }: WavyProgressProps) {\n  const id = useId();\n  const known = value !== undefined;\n  const pct = known ? Math.min(100, Math.max(0, (value / max) * 100)) : 30;\n  const done = known && pct >= 100;\n  const text = `${Math.round(pct)}%`;\n  const a11y = {\n    role: \"progressbar\",\n    \"aria-label\": label,\n    \"aria-valuemin\": 0,\n    \"aria-valuemax\": max,\n    ...(known ? { \"aria-valuenow\": value, \"aria-valuetext\": text } : {}),\n  } as const;\n\n  if (variant === \"circular\") {\n    const c = size / 2;\n    const stroke = Math.max(2.5, size / 16);\n    const r = c - stroke - 2;\n    // About one crest every 9px of circumference, low enough to read as a ripple on the ring.\n    const waves = Math.max(8, Math.round((2 * Math.PI * r) / 9));\n    return (\n      <div\n        data-slot=\"wavy-progress\"\n        data-variant=\"circular\"\n        data-indeterminate={known ? undefined : \"\"}\n        data-done={done ? \"\" : undefined}\n        className={cn(\"relative inline-grid place-items-center\", className)}\n        style={{ width: size, height: size, ...style }}\n        {...a11y}\n        {...props}\n      >\n        <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size} aria-hidden=\"true\">\n          <mask id={`${id}-arc`}>\n            <circle data-slot=\"wv-arc\" cx={c} cy={c} r={r} pathLength={100} fill=\"none\" stroke=\"#fff\" strokeWidth={stroke * 4} strokeDasharray={`${pct} 100`} style={{ transformOrigin: \"center\" }} />\n          </mask>\n          <circle data-slot=\"wv-track\" cx={c} cy={c} r={r} fill=\"none\" strokeWidth={stroke} pathLength={100} strokeDasharray={known ? `0 ${pct + 4} ${Math.max(0, 92 - pct)} 100` : undefined} />\n          <g mask={`url(#${id}-arc)`}>\n            <path data-slot=\"wv-wave\" d={ringPath(r, stroke * 0.42, waves, c)} fill=\"none\" strokeWidth={stroke} style={{ transformOrigin: \"center\" }} />\n            <circle data-slot=\"wv-flat\" cx={c} cy={c} r={r} fill=\"none\" strokeWidth={stroke} />\n          </g>\n        </svg>\n        {showValue && known && <span data-slot=\"wv-value\" className=\"absolute text-[11px] font-medium tabular-nums\">{text}</span>}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"wavy-progress\"\n      data-variant=\"linear\"\n      data-indeterminate={known ? undefined : \"\"}\n      data-done={done ? \"\" : undefined}\n      className={cn(\"flex w-full items-center gap-3\", className)}\n      style={{ \"--wv-pct\": `${pct}%`, ...style } as CSSProperties}\n      {...a11y}\n      {...props}\n    >\n      <span data-slot=\"wv-bar\" className=\"relative h-3 min-w-0 flex-1\" aria-hidden=\"true\">\n        <span data-slot=\"wv-fill\">\n          <span data-slot=\"wv-wave\" />\n          <span data-slot=\"wv-flat\" />\n        </span>\n        <span data-slot=\"wv-rest\" />\n      </span>\n      {showValue && known && <span data-slot=\"wv-value\" className=\"w-9 text-right text-xs tabular-nums text-muted-foreground\">{text}</span>}\n    </div>\n  );\n}\n"},{"path":"ui/wavy-progress.css","type":"registry:file","target":"components/ui/wavy-progress.css","content":"/* wavy-progress.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Wavy progress: the done part is a masked wave of ink that travels; it lies flat when done and under reduced motion. */\n  [data-slot=\"wavy-progress\"] { color: var(--foreground); --wv-track: color-mix(in oklab, var(--foreground) 14%, transparent); }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-fill\"] { position: absolute; inset: 0 auto 0 0; width: var(--wv-pct); transition: width var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-fill\"] > [data-slot=\"wv-wave\"] { position: absolute; inset: 0; background: currentColor; mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='12'%3E%3Cpath d='M0 6C4 .8 8 .8 12 6S20 11.2 24 6' fill='none' stroke='%23000' stroke-width='3.4'/%3E%3C/svg%3E\") 0 0 / 24px 12px repeat-x; animation: wv-travel 1s linear infinite; transition: opacity var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-fill\"] > [data-slot=\"wv-flat\"] { position: absolute; inset: 4px 0; border-radius: 999px; background: currentColor; opacity: 0; transition: opacity var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-rest\"] { position: absolute; top: 4px; right: 0; bottom: 4px; left: min(100%, calc(var(--wv-pct) + 6px)); border-radius: 999px; background: var(--wv-track); transition: left var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-rest\"]::after { content: \"\"; position: absolute; top: 0; right: 0; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }\n  [data-slot=\"wavy-progress\"][data-done] [data-slot=\"wv-wave\"] { opacity: 0; }\n  [data-slot=\"wavy-progress\"][data-done] [data-slot=\"wv-flat\"] { opacity: 1; }\n  [data-slot=\"wavy-progress\"][data-done] [data-slot=\"wv-rest\"] { opacity: 0; }\n  [data-slot=\"wavy-progress\"][data-variant=\"linear\"][data-indeterminate] [data-slot=\"wv-fill\"] { width: 34%; animation: wv-sweep 1.6s var(--ease-in-out) infinite; }\n  [data-slot=\"wavy-progress\"][data-variant=\"linear\"][data-indeterminate] [data-slot=\"wv-rest\"] { left: 0; }\n  [data-slot=\"wavy-progress\"][data-variant=\"linear\"][data-indeterminate] [data-slot=\"wv-rest\"]::after { display: none; }\n  [data-slot=\"wavy-progress\"][data-variant=\"linear\"][data-indeterminate] [data-slot=\"wv-bar\"] { overflow: hidden; }\n  [data-slot=\"wavy-progress\"][data-variant=\"circular\"] > svg { rotate: -90deg; overflow: visible; }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-track\"] { stroke: var(--wv-track); stroke-linecap: round; transition: stroke-dasharray var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-arc\"] { transition: stroke-dasharray var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] path[data-slot=\"wv-wave\"] { stroke: currentColor; stroke-linejoin: round; animation: wv-turn 7s linear infinite; transition: opacity var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"] circle[data-slot=\"wv-flat\"] { stroke: currentColor; opacity: 0; transition: opacity var(--duration-deliberate) var(--ease-out); }\n  [data-slot=\"wavy-progress\"][data-variant=\"circular\"][data-done] path[data-slot=\"wv-wave\"] { opacity: 0; }\n  [data-slot=\"wavy-progress\"][data-variant=\"circular\"][data-done] circle[data-slot=\"wv-flat\"] { opacity: 1; }\n  [data-slot=\"wavy-progress\"][data-variant=\"circular\"][data-indeterminate] [data-slot=\"wv-arc\"] { animation: wv-turn 1.3s linear infinite; }\n  [data-slot=\"wavy-progress\"] [data-slot=\"wv-value\"] { color: var(--muted-foreground); }\n  [data-slot=\"wavy-progress\"][data-variant=\"circular\"] [data-slot=\"wv-value\"] { color: var(--foreground); }\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"wavy-progress\"] [data-slot=\"wv-wave\"] { opacity: 0; animation: none; }\n    [data-slot=\"wavy-progress\"] [data-slot=\"wv-flat\"] { opacity: 1; }\n    [data-slot=\"wavy-progress\"][data-variant=\"linear\"][data-indeterminate] [data-slot=\"wv-fill\"] { animation-duration: 3.2s; }\n  }\n  @keyframes wv-travel { to { mask-position: 24px 0; } }\n  @keyframes wv-sweep { from { translate: -100% 0; } to { translate: 300% 0; } }\n  @keyframes wv-turn { to { rotate: 360deg; } }\n}\n"}],"categories":["motion"],"meta":{"tier":"free","license":"MIT","family":"changing-values","docs":"/docs/components/wavy-progress"}}