{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glaze-crawl-heal",
  "title": "Glaze Crawl Heal",
  "description": "A live status badge drawn as a glaze surface mid-firing: gas bubbles burst through the molten glaze and the melt either flows back over the crater (heals) or, when surface tension has failed, pulls away into a ragged bare patch that never closes (crawling). Three real ceramics behaviours standing in for operational, degraded and down.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/glaze-crawl-heal/component.tsx",
      "content": "\"use client\";\n\nimport { useLayoutEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// GlazeCrawlHeal — a live status badge drawn as a glaze surface mid-firing,\n// not three arbitrary colour chips. In a kiln the glaze goes molten and gas\n// bubbles rise through the melt and burst at the surface, leaving craters;\n// whether the melt's viscosity and surface tension let it FLOW BACK over\n// that crater (heals smooth) or not (crawling — the glaze pulls away from\n// the body into a bare patch that never closes) is a real, named ceramics\n// defect, not an invented metaphor. That gives the badge one physical state\n// axis instead of a colour code: healthy = bubbles burst and heal in well\n// under a second; degraded = the melt is thicker, craters linger for\n// seconds before closing; down = surface tension has failed outright —\n// bubbles still burst (gas still escapes) but the melt never flows back, so\n// ragged, beaded-rim bare patches accumulate and persist. Unlike craze-rule\n// (a static fracture-crack divider that draws in once and idles), this has\n// no crack geometry at all — it is continuous bubble nucleation, bursting\n// and healing/failing-to-heal, forever, because the glaze stays molten for\n// as long as the system is up. The canvas is pure decoration on top of a\n// real, always-present text status (STATUS_LABEL) inside a role=\"status\"\n// aria-live region — colour never carries the message alone. Ink is read\n// once via getComputedStyle(canvas).color (the canvas carries text-\n// foreground) in useLayoutEffect, before first paint, and re-read on a\n// documentElement class MutationObserver; craters/patches are punched with\n// globalCompositeOperation=\"destination-out\" rather than painted in a\n// second token, so a \"hole\" always reveals whatever sits behind the badge —\n// dark-on-light in light theme, light-on-dark in dark, for free. Zero deps,\n// one DPR-capped canvas, one rAF loop paused on prefers-reduced-motion,\n// tab-hidden and unmount.\n// ---------------------------------------------------------------------------\n\nexport type GlazeStatus = \"healthy\" | \"degraded\" | \"down\";\n\nexport interface GlazeCrawlHealProps {\n  /** the kiln state to render. default \"healthy\". */\n  status?: GlazeStatus;\n  /**\n   * accessible text announced via the component's own role=\"status\" region\n   * on every change. Defaults to the plain status name (\"Operational\").\n   */\n  label?: string;\n  /** render `label` as a visible text node beside the badge (still announced either way). default true — a status control's text is not optional. */\n  showLabel?: boolean;\n  /** badge diameter in px. Reads down to ~14; below that the mechanic itself stops resolving and only the text still carries the state — see component header. default 16. */\n  size?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst STATUS_LABEL: Record<GlazeStatus, string> = {\n  healthy: \"Operational\",\n  degraded: \"Degraded\",\n  down: \"Down\",\n};\n\nconst TAU = Math.PI * 2;\nconst JAG_POINTS = 11; // ragged-edge vertex count for a crawled (bare) patch\n\n// Per-status kiln parameters. healMs is the viscosity/surface-tension read:\n// short = fluid enough to close fast, long = it only just manages to close.\n// A \"down\" patch never gets a healMs of its own — it doesn't heal — until\n// the badge recovers to a status that has one (see the status-change effect\n// below), at which point the melt finally flows back over it.\nconst BUBBLE_CONFIG: Record<\n  \"healthy\" | \"degraded\",\n  {\n    spawnMs: [number, number];\n    openMs: number;\n    healMs: [number, number];\n    maxRFrac: number;\n    maxConcurrent: number;\n  }\n> = {\n  healthy: { spawnMs: [650, 1500], openMs: 150, healMs: [350, 550], maxRFrac: 0.3, maxConcurrent: 1 },\n  degraded: { spawnMs: [1100, 2200], openMs: 220, healMs: [1700, 3100], maxRFrac: 0.42, maxConcurrent: 2 },\n};\nconst PATCH_CONFIG = {\n  spawnMs: [900, 1800] as [number, number],\n  creepTauMs: 1300,\n  maxRFrac: 0.52,\n  maxConcurrent: 3,\n};\n\nfunction easeOutCubic(t: number) {\n  const p = 1 - t;\n  return 1 - p * p * p;\n}\nfunction easeInOutCubic(t: number) {\n  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n}\n\n/** mulberry32 — small, fast, deterministic given a seed */\nfunction mulberry32(seed: number) {\n  let s = seed | 0;\n  return () => {\n    s = (s + 0x6d2b79f5) | 0;\n    let t = Math.imul(s ^ (s >>> 15), 1 | s);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** per-vertex radius multipliers (0.72..1.06) for a crawled patch's ragged rim */\nfunction makeJag(seed: number): number[] {\n  const rng = mulberry32(seed);\n  const out: number[] = [];\n  for (let i = 0; i < JAG_POINTS; i++) out.push(0.72 + rng() * 0.34);\n  return out;\n}\n\n// fixed seeds -> deterministic, byte-stable jag shapes for the\n// prefers-reduced-motion freeze frame (no Math.random there, ever).\nconst DOWN_JAG_A = makeJag(101);\nconst DOWN_JAG_B = makeJag(202);\n\ntype Feature = {\n  kind: \"bubble\" | \"patch\";\n  x: number;\n  y: number; // offset from disc centre, CSS px\n  maxR: number; // CSS px\n  bornAt: number;\n  openMs: number;\n  healMs: number; // set once healing begins; Infinity/unused until then\n  healingSince: number | null;\n  healStartR: number;\n  creepTau: number;\n  wobblePhase: number;\n  jag: number[] | null; // null = perfect circle (bubble); set = ragged patch\n};\n\ntype DrawSpec = {\n  x: number;\n  y: number;\n  r: number;\n  jag: number[] | null;\n  rimAlpha: number;\n  flash: number;\n};\n\nfunction drawFeature(\n  ctx: CanvasRenderingContext2D,\n  ink: string,\n  cx: number,\n  cy: number,\n  R0: number,\n  f: DrawSpec,\n) {\n  if (f.r <= 0.3) return;\n  ctx.save();\n  ctx.translate(cx + f.x, cy + f.y);\n\n  ctx.globalCompositeOperation = \"destination-out\";\n  ctx.beginPath();\n  if (f.jag) {\n    for (let i = 0; i < JAG_POINTS; i++) {\n      const a = (i / JAG_POINTS) * TAU;\n      const rad = f.r * f.jag[i]!;\n      const px = Math.cos(a) * rad;\n      const py = Math.sin(a) * rad;\n      if (i === 0) ctx.moveTo(px, py);\n      else ctx.lineTo(px, py);\n    }\n    ctx.closePath();\n  } else {\n    ctx.arc(0, 0, f.r, 0, TAU);\n  }\n  ctx.fill();\n\n  ctx.globalCompositeOperation = \"source-over\";\n  if (f.jag) {\n    // beaded rim: glaze that pulled away balls up at the receding edge\n    // instead of flowing back — the visible signature of crawling.\n    ctx.fillStyle = ink;\n    for (let i = 0; i < JAG_POINTS; i++) {\n      const a = (i / JAG_POINTS) * TAU;\n      const rad = f.r * f.jag[i]!;\n      const px = Math.cos(a) * rad;\n      const py = Math.sin(a) * rad;\n      ctx.globalAlpha = 0.5 + 0.35 * ((i * 5) % 4) / 4;\n      ctx.beginPath();\n      ctx.arc(px, py, Math.max(0.55, R0 * 0.045), 0, TAU);\n      ctx.fill();\n    }\n    ctx.globalAlpha = 1;\n  } else if (f.rimAlpha > 0) {\n    // healing crater's raised lip\n    ctx.strokeStyle = ink;\n    ctx.globalAlpha = f.rimAlpha;\n    ctx.lineWidth = Math.max(0.6, R0 * 0.05);\n    ctx.beginPath();\n    ctx.arc(0, 0, f.r + R0 * 0.02, 0, TAU);\n    ctx.stroke();\n    ctx.globalAlpha = 1;\n  }\n  if (f.flash > 0) {\n    ctx.strokeStyle = ink;\n    ctx.globalAlpha = f.flash;\n    ctx.lineWidth = Math.max(0.8, R0 * 0.09);\n    ctx.beginPath();\n    ctx.arc(0, 0, f.r * 1.02, 0, TAU);\n    ctx.stroke();\n    ctx.globalAlpha = 1;\n  }\n  ctx.restore();\n}\n\nfunction renderFrame(\n  ctx: CanvasRenderingContext2D,\n  ink: string,\n  w: number,\n  h: number,\n  cx: number,\n  cy: number,\n  R0: number,\n  sheenAngle: number,\n  specs: DrawSpec[],\n) {\n  ctx.clearRect(0, 0, w, h);\n  ctx.globalCompositeOperation = \"source-over\";\n  ctx.fillStyle = ink;\n  ctx.globalAlpha = 1;\n  ctx.beginPath();\n  ctx.arc(cx, cy, R0, 0, TAU);\n  ctx.fill();\n\n  // a slow-drifting specular dim patch — the melt still reads as liquid in\n  // the gaps between bubble events, never a flat dead disc.\n  ctx.globalCompositeOperation = \"destination-out\";\n  const sx = cx + Math.cos(sheenAngle) * R0 * 0.5;\n  const sy = cy + Math.sin(sheenAngle) * R0 * 0.5;\n  ctx.globalAlpha = 0.08;\n  ctx.beginPath();\n  ctx.ellipse(sx, sy, R0 * 0.42, R0 * 0.3, sheenAngle, 0, TAU);\n  ctx.fill();\n  ctx.globalAlpha = 1;\n  ctx.globalCompositeOperation = \"source-over\";\n\n  for (const f of specs) drawFeature(ctx, ink, cx, cy, R0, f);\n}\n\n/** deterministic freeze frame per status — craters/patches at varied stages of healing, never mid-t0 */\nfunction staticSpecs(status: GlazeStatus, R0: number): DrawSpec[] {\n  if (status === \"healthy\") {\n    const maxR = R0 * BUBBLE_CONFIG.healthy.maxRFrac;\n    return [\n      { x: -R0 * 0.3, y: -R0 * 0.12, r: maxR * 0.55, jag: null, rimAlpha: 0.35, flash: 0 },\n      { x: R0 * 0.34, y: R0 * 0.28, r: maxR * 0.15, jag: null, rimAlpha: 0.45, flash: 0 },\n    ];\n  }\n  if (status === \"degraded\") {\n    const maxR = R0 * BUBBLE_CONFIG.degraded.maxRFrac;\n    return [\n      { x: -R0 * 0.3, y: -R0 * 0.22, r: maxR * 0.92, jag: null, rimAlpha: 0.5, flash: 0 },\n      { x: R0 * 0.28, y: R0 * 0.1, r: maxR * 0.55, jag: null, rimAlpha: 0.4, flash: 0 },\n      { x: 0, y: R0 * 0.36, r: maxR * 0.2, jag: null, rimAlpha: 0.45, flash: 0 },\n    ];\n  }\n  const maxR = R0 * PATCH_CONFIG.maxRFrac;\n  return [\n    { x: -R0 * 0.24, y: -R0 * 0.18, r: maxR * 0.9, jag: DOWN_JAG_A, rimAlpha: 0, flash: 0 },\n    { x: R0 * 0.3, y: R0 * 0.26, r: maxR * 0.4, jag: DOWN_JAG_B, rimAlpha: 0, flash: 0 },\n  ];\n}\n\nexport function GlazeCrawlHeal({\n  status = \"healthy\",\n  label,\n  showLabel = true,\n  size = 16,\n  className = \"\",\n}: GlazeCrawlHealProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const engineRef = useRef<{ setStatus: (s: GlazeStatus) => void } | null>(null);\n\n  // -- token derivation happens before first paint, so nothing ever paints\n  // with a default/unset ink colour --\n  useLayoutEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let ink = \"\";\n    let w = 0;\n    let h = 0;\n    let cx = 0;\n    let cy = 0;\n    let R0 = 0;\n    let raf = 0;\n    let nextSpawnAt = 0;\n    let statusCur: GlazeStatus = status;\n    const features: Feature[] = [];\n\n    const readInk = () => {\n      ink = getComputedStyle(canvas).color;\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = size;\n      h = size;\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      cx = w / 2;\n      cy = h / 2;\n      R0 = (Math.min(w, h) / 2) * 0.86;\n    };\n\n    const drawStatic = () => {\n      renderFrame(ctx, ink, w, h, cx, cy, R0, -0.8, staticSpecs(statusCur, R0));\n    };\n\n    const maxRFor = (kind: \"healthy\" | \"degraded\" | \"down\") =>\n      R0 * (kind === \"down\" ? PATCH_CONFIG.maxRFrac : BUBBLE_CONFIG[kind].maxRFrac);\n\n    let featureSeed = 1;\n\n    const maybeSpawn = (now: number) => {\n      if (now < nextSpawnAt) return;\n      if (statusCur === \"down\") {\n        if (features.length < PATCH_CONFIG.maxConcurrent) {\n          const margin = Math.max(0, R0 * 0.9 - maxRFor(\"down\"));\n          const ang = Math.random() * TAU;\n          const dist = Math.random() * margin;\n          features.push({\n            kind: \"patch\",\n            x: Math.cos(ang) * dist,\n            y: Math.sin(ang) * dist,\n            maxR: maxRFor(\"down\") * (0.6 + Math.random() * 0.4),\n            bornAt: now,\n            openMs: 0,\n            healMs: 0,\n            healingSince: null,\n            healStartR: 0,\n            creepTau: PATCH_CONFIG.creepTauMs * (0.8 + Math.random() * 0.5),\n            wobblePhase: Math.random() * TAU,\n            jag: makeJag(featureSeed++),\n          });\n        }\n        const [a, b] = PATCH_CONFIG.spawnMs;\n        nextSpawnAt = now + a + Math.random() * (b - a);\n        return;\n      }\n      const cfg = BUBBLE_CONFIG[statusCur];\n      if (features.length < cfg.maxConcurrent) {\n        const margin = Math.max(0, R0 * 0.9 - maxRFor(statusCur));\n        const ang = Math.random() * TAU;\n        const dist = Math.random() * margin;\n        const [hMin, hMax] = cfg.healMs;\n        features.push({\n          kind: \"bubble\",\n          x: Math.cos(ang) * dist,\n          y: Math.sin(ang) * dist,\n          maxR: R0 * cfg.maxRFrac * (0.7 + Math.random() * 0.5),\n          bornAt: now,\n          openMs: cfg.openMs,\n          healMs: hMin + Math.random() * (hMax - hMin),\n          healingSince: null,\n          healStartR: 0,\n          creepTau: 0,\n          wobblePhase: 0,\n          jag: null,\n        });\n      }\n      const [a, b] = cfg.spawnMs;\n      nextSpawnAt = now + a + Math.random() * (b - a);\n    };\n\n    const tick = (now: number) => {\n      raf = requestAnimationFrame(tick);\n      if (document.hidden) return;\n\n      maybeSpawn(now);\n\n      const specs: DrawSpec[] = [];\n      for (let i = features.length - 1; i >= 0; i--) {\n        const f = features[i]!;\n        const age = now - f.bornAt;\n        let r = 0;\n        let rimAlpha = 0;\n        let flash = 0;\n        let dead = false;\n\n        if (f.kind === \"bubble\") {\n          if (f.healingSince == null) {\n            if (age < f.openMs) {\n              r = f.maxR * easeOutCubic(f.openMs > 0 ? age / f.openMs : 1);\n            } else {\n              f.healingSince = f.bornAt + f.openMs;\n            }\n          }\n          if (f.healingSince != null) {\n            const hAge = now - f.healingSince;\n            if (hAge >= f.healMs) {\n              dead = true;\n            } else {\n              r = f.maxR * (1 - easeInOutCubic(hAge / f.healMs));\n              rimAlpha = 0.15 + 0.35 * (1 - hAge / f.healMs);\n            }\n          }\n          const peak = f.bornAt + f.openMs;\n          const df = Math.abs(now - peak);\n          if (df < 120) flash = 1 - df / 120;\n        } else {\n          // patch — burst once (gas still escapes) then either creeps\n          // (still failing to close) or, post-recovery, heals like a bubble\n          if (f.healingSince == null) {\n            r = f.maxR * (1 - Math.exp(-age / f.creepTau));\n            r *= 1 + 0.03 * Math.sin(age * 0.0025 + f.wobblePhase);\n            if (age < 130) flash = 1 - age / 130;\n          } else {\n            const hAge = now - f.healingSince;\n            if (hAge >= f.healMs) {\n              dead = true;\n            } else {\n              r = f.healStartR * (1 - easeInOutCubic(hAge / f.healMs));\n            }\n          }\n        }\n\n        if (dead) {\n          features.splice(i, 1);\n          continue;\n        }\n        specs.push({ x: f.x, y: f.y, r, jag: f.jag, rimAlpha, flash });\n      }\n\n      const sheenAngle = now * 0.00015;\n      renderFrame(ctx, ink, w, h, cx, cy, R0, sheenAngle, specs);\n    };\n\n    const start = () => {\n      if (reduced || raf) return;\n      raf = requestAnimationFrame(tick);\n    };\n    const stop = () => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    engineRef.current = {\n      setStatus: (next) => {\n        const prev = statusCur;\n        statusCur = next;\n        if (prev === \"down\" && next !== \"down\") {\n          // conditions recovered — the melt can finally flow back over\n          // whatever crawled while it was down.\n          const now = performance.now();\n          const cfg = BUBBLE_CONFIG[next];\n          const [hMin, hMax] = cfg.healMs;\n          for (const f of features) {\n            if (f.kind === \"patch\" && f.healingSince == null) {\n              const age = now - f.bornAt;\n              const r =\n                f.maxR *\n                (1 - Math.exp(-age / f.creepTau)) *\n                (1 + 0.03 * Math.sin(age * 0.0025 + f.wobblePhase));\n              f.healStartR = r;\n              f.healingSince = now;\n              f.healMs = hMin + Math.random() * (hMax - hMin);\n            }\n          }\n        }\n        if (reduced) drawStatic();\n        else start();\n      },\n    };\n\n    const onVis = () => {\n      if (!document.hidden) start();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    const mo = new MutationObserver(() => {\n      readInk();\n      if (reduced) drawStatic();\n    });\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    readInk();\n    resize();\n    if (reduced) drawStatic();\n    else start();\n\n    return () => {\n      engineRef.current = null;\n      stop();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [size]);\n\n  useLayoutEffect(() => {\n    engineRef.current?.setStatus(status);\n  }, [status]);\n\n  const text = label ?? STATUS_LABEL[status];\n\n  return (\n    <span\n      role=\"status\"\n      aria-live=\"polite\"\n      data-glaze-status={status}\n      className={`inline-flex shrink-0 items-center gap-2 ${className}`}\n    >\n      <span\n        aria-hidden\n        className=\"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border\"\n        style={{ width: size, height: size }}\n      >\n        <canvas ref={canvasRef} className=\"block text-foreground\" />\n      </span>\n      {showLabel ? (\n        <span className=\"font-mono text-xs text-foreground\">{text}</span>\n      ) : (\n        <span className=\"sr-only\">{text}</span>\n      )}\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/glaze-crawl-heal.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "status",
      "badge",
      "indicator",
      "canvas",
      "ambient",
      "aria-live",
      "role-status"
    ],
    "instruction": "Build <GlazeCrawlHeal status? label? showLabel? size? className?> as a live status badge sourced from real glaze-firing behaviour, not an invented colour code. GLAZE PHYSICS: in a kiln the glaze goes molten and gas bubbles rise through the melt and burst at the surface, leaving craters; whether the melt's viscosity and surface tension let it flow back over the crater (heal) or not (crawling — the glaze pulls away from the clay body into a bare patch that never closes) is a documented ceramics defect, and it is the component's entire state axis: healthy = bubbles burst and heal in well under a second (short healMs, thin/fluid melt), degraded = the melt is thicker so craters linger for one to three seconds before closing (long healMs, same mechanic, different viscosity), down = surface tension has failed outright — bubbles still burst (gas still escapes the melt) but the melt never flows back, so ragged, beaded-rim bare patches accumulate and persist until conditions recover. RENDERING: one DPR-capped <canvas> circle inset inside a border-border ring. Every frame redraws a solid disc filled with the canvas's own colour (getComputedStyle(canvas).color, the canvas carries text-foreground), then punches each live bubble/patch into that disc via globalCompositeOperation='destination-out' at its current radius — a perfect circle for a bubble, an 11-vertex ragged polygon (per-vertex radius 0.72-1.06x nominal, from a seeded mulberry32 walk) for a crawled patch. Punching with destination-out rather than painting a second token means a 'hole' always reveals whatever is genuinely behind the badge, so molten ink is dark-on-light in light theme and light-on-dark in dark theme for free, with no theme branch in the draw code. A healing crater gets a thin foreground rim stroke (its raised lip) that fades as it closes; a crawled patch gets small foreground beads at its ragged vertices instead (glaze balled up at the receding edge rather than flowing back) and never gets a rim, because it isn't healing. A slow-drifting, low-alpha destination-out ellipse (0.08 alpha, ~0.00015 rad/ms) sweeps across the disc as a specular sheen so the surface still reads as liquid in the seconds between bubble events. LIFECYCLE: a bubble opens 0->maxR over openMs (ease-out-cubic, the pop) then closes maxR->0 over a per-status randomised healMs (ease-in-out-cubic) and is removed; a momentary full-strength rim flash brackets the peak, selling the burst. A down-state patch bursts open the same way but then creeps 0->maxR on an exponential approach (r = maxR*(1-e^(-age/tau)), tau ~1.3s) with a small ambient wobble, asymptotically approaching but never reaching maxR — it simply never gets a healMs, so it never closes, which is what makes 'never heals' true rather than merely slow. Concurrency and nucleation cadence differ per status (healthy: 1 bubble, 650-1500ms between spawns; degraded: up to 2, 1100-2200ms; down: up to 3 patches, 900-1800ms) so the badge is genuinely alive at rest at every status, with no settled end state — screenshots taken a mount, +2.5s and +5s all differ. RECOVERY: when status transitions away from 'down', every live patch is handed a real healMs drawn from the new status's own healthy/degraded range and its current creeping radius becomes its heal-start radius, so it finally flows back closed on the same ease-in-out curve a bubble uses — the melt genuinely repairs itself once conditions are fixed, it does not just vanish. A11Y: the root is a <span role='status' aria-live='polite'> so every status change is announced by assistive tech; the canvas is aria-hidden and is decoration only — the accessible name and the announced text are one real string (STATUS_LABEL: 'Operational' / 'Degraded' / 'Down', or a caller-supplied label), rendered as a visible text node by default (showLabel) or as a visually-hidden span when showLabel is false, so a colour-blind or screen-reader user always gets the same information a sighted user gets from the canvas. No throttling logic is needed beyond that — status is caller-driven, discrete state, not a per-frame value. TOKENS: ink is read once via getComputedStyle(canvas).color in useLayoutEffect, before first paint, and re-read on a documentElement class MutationObserver; every stroke/fill on the canvas is that one ink value composited with alpha, so nothing is a second colour — --ns-accent never appears, because this is ambient state, not interaction chrome, and the badge's own outline ring is the one legitimate --border use. SCALE: default size is 16px, a realistic inline status-dot scale, and the mechanic (a bursting/healing/crawling disc) is proportioned to read down to about 14px; below roughly 10-12px the ragged-patch texture and rim detail stop resolving to the eye and the text label becomes the only reliable channel — stated plainly rather than hidden, which is exactly why the text is never optional to the accessibility tree. REDUCED MOTION: window.matchMedia('(prefers-reduced-motion: reduce)') freezes the canvas on one deterministic, non-t0 still per status built from fixed offsets/radii (and, for 'down', two fixed-seed ragged shapes) rather than any live simulation state — healthy shows one crater mid-heal and one nearly closed, degraded shows three craters at clearly different open/closing stages, down shows one large stalled patch and one smaller forming one — so the mechanic is legible in a single frame and the frame is byte-stable across repeated captures; status text and the aria-live region stay fully live regardless of the motion preference. Props: status ('healthy' | 'degraded' | 'down', default 'healthy'), label (accessible override), showLabel (default true), size (px, default 16), className. Zero dependencies, no canvas resize observer needed since size is a controlled prop rather than a fluid container."
  },
  "type": "registry:ui"
}