{
  "name": "flames",
  "type": "registry:ui",
  "title": "Flames",
  "description": "Small animated bars proving activity is flowing. CSS grade for list rows (cheap, many); signal-driven grade whose bar heights follow a real level.",
  "dependencies": [
    "@candlelight-ds/tokens",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/instruments/Flames.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\n/**\n * Flames — the activity indicator, two grades:\n * - Flames: CSS-animated, for list rows (cheap, many instances)\n * - SignalFlames: signal-driven, bar heights follow a real level you\n *   supply (one or two instances per surface: the console, the stage)\n *\n * Flames mean activity — sound flowing, a job running. Never decorate\n * an idle state with them. Both grades die under prefers-reduced-motion\n * (the CSS grade goes static; drive the signal grade's `active` off the\n * media query if the level source keeps ticking).\n *\n * Use when: something is audibly/visibly running.\n * Don't use when: decoration.\n */\n\nimport { useEffect, useRef } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport function Flames({ className }: { className?: string }) {\n  return (\n    <span aria-hidden className={cn('flex h-3.5 items-end gap-[2px]', className)}>\n      <span className=\"flame-bar\" data-active />\n      <span className=\"flame-bar\" data-active />\n      <span className=\"flame-bar\" data-active />\n    </span>\n  )\n}\n\ninterface SignalFlamesProps {\n  /** Whether activity is currently flowing — flames hide otherwise. */\n  active: boolean\n  /** Returns the current level, 0..1 — polled per animation frame. */\n  getLevel: () => number\n  bars?: number\n  className?: string\n}\n\nexport function SignalFlames({ active, getLevel, bars = 5, className }: SignalFlamesProps) {\n  const barsRef = useRef<Array<HTMLSpanElement | null>>([])\n\n  useEffect(() => {\n    if (!active) return\n    let raf = 0\n    const multipliers = [0.7, 1, 0.85, 1.1, 0.6, 0.95, 0.8]\n    const tick = () => {\n      const level = getLevel()\n      barsRef.current.forEach((bar, index) => {\n        if (!bar) return\n        // scaleY, not height: per-frame height writes relayout; the\n        // transform rides the compositor. Bars are full height and\n        // bottom-anchored, so the squash reads as the level.\n        const scale =\n          (3 + Math.min(1, level * 2.6 * multipliers[index % multipliers.length]) * 13) / 16\n        bar.style.transform = `scaleY(${scale})`\n      })\n      raf = requestAnimationFrame(tick)\n    }\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [active, getLevel])\n\n  if (!active) return null\n\n  return (\n    <span aria-hidden className={cn('flex h-4 items-end gap-[2.5px]', className)}>\n      {Array.from({ length: bars }, (_, index) => (\n        <span\n          key={index}\n          ref={(el) => {\n            barsRef.current[index] = el\n          }}\n          className=\"h-4 w-[2.5px] origin-bottom rounded-full bg-brass/85\"\n          style={{ transform: 'scaleY(0.25)', transition: 'transform 90ms ease-out' }}\n        />\n      ))}\n    </span>\n  )\n}\n",
      "target": "components/instruments/Flames.tsx"
    }
  ],
  "meta": {
    "candlelight": {
      "category": "instrument",
      "gRules": [
        "G7"
      ],
      "useWhen": "Something is audibly/visibly running — a track, a job, a broadcast.",
      "dontUseWhen": "Decoration. Flames mean activity; an idle state never wears them.",
      "states": [
        "active (three CSS bars)",
        "signal-driven (scaleY follows the level)",
        "hidden when idle",
        "static under reduced motion"
      ],
      "keyboard": [],
      "acceptanceTests": [
        "aria-hidden — flames are a visual double of state that exists elsewhere in text.",
        "The CSS loop dies under prefers-reduced-motion (flame-bar)."
      ]
    }
  },
  "registryDependencies": [
    "utils"
  ]
}