{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-midi-sonar",
  "type": "registry:ui",
  "title": "MIDI Sonar",
  "description": "Circular MIDI sonar and note orbits: held notes stay bright, released notes fade, and the host owns time.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/midi-sonar.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/midi-sonar\n * Copyright © [Maru](https://maru.audio)\n *\n * Maru UI Kit — https://maru.audio/ui-kit\n *\n * Installed from the Maru UI registry. This file is yours now: edit it\n * freely, it will not be overwritten unless you re-run the add command.\n *\n * Keep this item’s companion files and CSS imports together. Dependencies\n * declared by the registry are installed with the source.\n */\nimport { useEffect, useRef, type CSSProperties } from \"react\"\nimport {\n  createMidiSonarRenderer,\n  type MidiSonarFrame,\n} from \"maruaudio/ui/shared/midi-sonar-renderer\"\n\nexport interface MidiSonarProps extends MidiSonarFrame {\n  readonly className?: string\n  readonly style?: CSSProperties\n  readonly height?: number | string\n  readonly \"aria-label\"?: string\n}\n\n/** Controlled by the host playhead. No MIDI device, transport, RAF or audio path. */\nexport function MidiSonar(props: MidiSonarProps) {\n  const canvas = useRef<HTMLCanvasElement>(null)\n  const current = useRef(props)\n  current.current = props\n  const paint = useRef<(() => void) | null>(null)\n  useEffect(() => {\n    const target = canvas.current\n    if (!target) return\n    const renderer = createMidiSonarRenderer(target)\n    const draw = () => {\n      if (document.hidden) return\n      const box = target.getBoundingClientRect()\n      if (!box.width || !box.height) return\n      const scale = Math.min(\n        2,\n        window.devicePixelRatio || 1,\n        Math.sqrt(1_000_000 / (box.width * box.height))\n      )\n      const width = Math.max(1, Math.floor(box.width * scale)),\n        height = Math.max(1, Math.floor(box.height * scale))\n      if (target.width !== width || target.height !== height)\n        renderer.resize(width, height)\n      const theme = getComputedStyle(target)\n      const token = (name: string, fallback: string) =>\n        theme.getPropertyValue(name).trim() || fallback\n      const p = current.current\n      renderer.draw({\n        ...p,\n        background: p.background ?? token(\"--uikit-surface\", \"#0c1517\"),\n        ink: p.ink ?? token(\"--uikit-text\", \"#eeeacb\"),\n        accent: p.accent ?? token(\"--uikit-accent\", \"#d6c76a\"),\n        muted: p.muted ?? token(\"--uikit-border\", \"#686b66\"),\n      })\n    }\n    paint.current = draw\n    const observer = new ResizeObserver(draw)\n    observer.observe(target)\n    document.addEventListener(\"visibilitychange\", draw)\n    draw()\n    return () => {\n      observer.disconnect()\n      document.removeEventListener(\"visibilitychange\", draw)\n      paint.current = null\n      renderer.dispose()\n    }\n  }, [])\n  useEffect(() => {\n    paint.current?.()\n  }, [props])\n  return (\n    <canvas\n      ref={canvas}\n      role=\"img\"\n      aria-label={\n        props[\"aria-label\"] ??\n        \"MIDI sonar: sounding notes and fading note-off trails\"\n      }\n      className={props.className}\n      style={{\n        display: \"block\",\n        width: \"100%\",\n        height: props.height ?? 300,\n        ...props.style,\n      }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/midi-sonar.tsx"
    }
  ]
}
