{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-producer-eq-panel",
  "type": "registry:ui",
  "title": "Producer EQ Panel",
  "description": "Host-bound producer equalizer composite over EQEditor: numbered band overview, descriptor-bound precise editing, factory preset navigation, momentary audition and real pre/post scope analysis. Product adapters supply band contracts and recipes. Requires the SDK and UI-kit host-control package; ships its scoped layout stylesheet beside the component.",
  "dependencies": [
    "@base-ui/react",
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-cc-control-chrome.json",
    "https://pluginui.com/r/core-console-button.json",
    "https://pluginui.com/r/core-console-switch.json",
    "https://pluginui.com/r/core-eq-editor.json",
    "https://pluginui.com/r/core-module-power-button.json",
    "https://pluginui.com/r/core-number-field.json"
  ],
  "files": [
    {
      "path": "components/ui/producer-eq-panel.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/producer-eq-panel\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 */\n/** Reusable producer EQ presentation; product schema and recipes enter as configuration. */\nimport { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from \"react\"\nimport { Button } from \"@base-ui/react/button\"\nimport type { AudioModuleFaceProps } from \"maruaudio/ui/shared/maru/face-preview\"\nimport { readModuleFaceValue } from \"maruaudio/ui/shared/maru/face-preview\"\nimport { EQEditor, type EQEditorBand, type BandPatch } from \"@/components/ui/eq-editor\"\nimport { ConsoleSwitch } from \"@/components/ui/console-switch\"\nimport { ConsoleButton } from \"@/components/ui/console-button\"\nimport { NumberField } from \"@/components/ui/number-field\"\nimport { CcControlChromeTarget } from \"@/components/ui/cc-control-chrome\"\nimport { FaceModuleNavbar, FaceParamSelect, faceParamBinding, isFaceModulePowered, useFaceParamEditGesture, type FaceFactoryPreset } from \"maruaudio/ui/shared/maru/module-face-controls\"\nimport { useEqAnalysis } from \"@/components/ui/use-eq-analysis\"\nimport { bindEqAuditionFocusLoss } from \"maruaudio/ui/shared/eq-audition-lifecycle\"\nimport \"@/components/ui/producer-eq-panel.css\"\n\nconst BAND_COLORS = [\"#f87171\", \"#fb923c\", \"#facc15\", \"#4ade80\", \"#38bdf8\", \"#a78bfa\", \"#f472b6\", \"#2dd4bf\"]\nconst frequencyText = (v: number) => v >= 1000 ? `${(v / 1000).toFixed(v >= 10000 ? 1 : 2)} kHz` : `${Math.round(v)} Hz`\nconst dbText = (v: number) => `${v > 0 ? \"+\" : \"\"}${v.toFixed(1)} dB`\n\nfunction ParamNumber({ face, path, label, unit, disabled = false }: { face: AudioModuleFaceProps; path: string; label: string; unit?: string; disabled?: boolean }) {\n  const binding = faceParamBinding(face, path)\n  const edit = useFaceParamEditGesture(face)\n  return <CcControlChromeTarget as=\"div\" path={path} label={label} allowEditableContextMenu className=\"peq-field\" presentation={{ label, path, value: `${binding.value} ${unit ?? \"\"}` }}>\n    <span>{label}</span>\n    <div data-audio-param-path={path}>\n      <NumberField value={binding.value} min={binding.min} max={binding.max} step={binding.step || (unit === \"Hz\" ? 1 : 0.1)} size=\"lg\" inputWidth=\"100%\" unit={unit} disabled={disabled} aria-label={label} format={{ maximumFractionDigits: unit === \"Hz\" ? 0 : 2 }} onValueCommitted={(next) => { if (next === null) return; edit.change(path, next); edit.end(path, next) }} />\n    </div>\n  </CcControlChromeTarget>\n}\n\nfunction ParamSelect({ face, path, label, disabled = false }: { face: AudioModuleFaceProps; path: string; label: string; disabled?: boolean }) {\n  const binding = faceParamBinding(face, path)\n  return <div className=\"peq-field peq-field--select\" data-audio-param-path={path}><FaceParamSelect face={face} path={path} label={label} disabled={disabled} triggerClassName=\"peq-select-trigger\" popupClassName=\"peq-select-popup\" groups={[{ label, options: (binding.options ?? []).map((name, value) => ({ label: name, value })) }]} /></div>\n}\n\nfunction BandEnable({ face, band, path }: { face: AudioModuleFaceProps; band: number; path: string }) {\n  const binding = faceParamBinding(face, path)\n  const edit = useFaceParamEditGesture(face)\n  const enabled = binding.value >= 0.5\n  return <CcControlChromeTarget as=\"div\" path={path} data-audio-param-path={path} label={`Band ${band + 1} enabled`} allowEditableContextMenu presentation={{ label: `Band ${band + 1} enabled`, path, value: enabled ? \"On\" : \"Off\" }}>\n    <div className=\"peq-enable\"><ConsoleSwitch checked={enabled} aria-label={`Band ${band + 1} enabled`} onCheckedChange={checked => { const next = checked ? 1 : 0; edit.change(path, next); edit.end(path, next) }} /></div>\n  </CcControlChromeTarget>\n}\n\n/** Momentary listening never leaves an audition parameter latched on focus loss. */\nfunction BandListen({ face, band, powered, path }: { face: AudioModuleFaceProps; band: number; powered: boolean; path: string }) {\n  const edit = useFaceParamEditGesture(face)\n  const active = useRef(false)\n  const stop = useCallback(() => { if (!active.current) return; active.current = false; edit.change(path, 0); edit.end(path, 0) }, [edit, path])\n  useEffect(() => { if (!powered) stop(); return stop }, [powered, band, stop])\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n    return bindEqAuditionFocusLoss(stop, window, document)\n  }, [stop])\n  const start = () => { if (!powered || active.current) return; active.current = true; edit.change(path, band + 1) }\n  return <ConsoleButton size=\"sm\" variant=\"ghost\" className=\"peq-reset\" aria-label={`Hold to listen to band ${band + 1}`} disabled={!powered} onPointerDown={event => { event.currentTarget.setPointerCapture(event.pointerId); start() }} onPointerUp={stop} onPointerCancel={stop} onLostPointerCapture={stop} onBlur={stop} onKeyDown={event => { if (event.key === \" \" || event.key === \"Enter\") { event.preventDefault(); start() } }} onKeyUp={event => { if (event.key === \" \" || event.key === \"Enter\") { event.preventDefault(); stop() } }}>Listen</ConsoleButton>\n}\n\n/** Actual control-rate detector/reduction telemetry, never inferred from the static curve. */\nfunction BandDynamicsStatus({ face, band, bandPath }: { face: AudioModuleFaceProps; band: number; bandPath: ProducerEqPanelProps[\"bandPath\"] }) {\n  const [status, setStatus] = useState<{ detector: number; reduction: number; headroom: number } | null>(null)\n  useEffect(() => {\n    setStatus(null)\n    if (!face.readVisualization || face.live === false || face.power?.powered === false) return\n    const update = () => {\n      if (typeof document !== \"undefined\" && document.hidden) return\n      const scalars = face.readVisualization?.()?.scalars\n      const detector = scalars?.[bandPath(band, \"detectorDb\")]\n      const reduction = scalars?.[bandPath(band, \"reductionDb\")]\n      const headroom = scalars?.[bandPath(band, \"headroomDb\")]\n      setStatus(typeof detector === \"number\" && Number.isFinite(detector) && typeof reduction === \"number\" && Number.isFinite(reduction) && typeof headroom === \"number\" && Number.isFinite(headroom) ? { detector, reduction, headroom } : null)\n    }\n    update()\n    const timer = setInterval(update, 125)\n    return () => clearInterval(timer)\n  }, [face.readVisualization, face.live, face.power?.powered, band, bandPath])\n  return <div className=\"peq-dynamics-status\" aria-label=\"Measured band dynamics\" aria-description={status ? `Detector ${status.detector.toFixed(1)} dBFS · Headroom ${status.headroom.toFixed(1)} dB` : \"Band dynamics telemetry unavailable\"}>{status ? `Reduction ${status.reduction.toFixed(1)} dB` : \"Dynamics unavailable\"}</div>\n}\n\nexport interface ProducerEqPanelProps {\n  readonly face: AudioModuleFaceProps\n  readonly bandCount: number\n  readonly title: string\n  readonly fallbackPresets?: readonly FaceFactoryPreset[]\n  readonly bandRoles?: readonly { readonly type: number; readonly label: string }[]\n  readonly bandPath: (band: number, field: string) => string\n  readonly bandTypeToEditor: (type: number) => EQEditorBand[\"type\"]\n  readonly bandUsesGain: (type: number) => boolean\n  readonly outputGainPath: string\n  readonly auditionBandPath?: string\n  /** Product-owned frequency clamp relative to processing sample rate. */\n  readonly frequencyLimitRatio?: number\n}\n\nexport function ProducerEqPanel({ face: props, bandCount, title, fallbackPresets = [], bandRoles = [], bandPath, bandTypeToEditor, bandUsesGain, outputGainPath, auditionBandPath, frequencyLimitRatio = 0.45 }: ProducerEqPanelProps) {\n  const bandLabel = (band: number) => {\n    const binding = faceParamBinding(props, bandPath(band, \"type\"))\n    return bandRoles[band]?.type === binding.value ? bandRoles[band]!.label : binding.options?.[Math.round(binding.value)] ?? `Band ${band + 1}`\n  }\n  const bandSummary = (band: number, view: EQEditorBand) => {\n    const type = Math.round(readModuleFaceValue(props, bandPath(band, \"type\")))\n    if (bandUsesGain(type)) return dbText(view.gain ?? readModuleFaceValue(props, bandPath(band, \"gain\")))\n    const path = bandPath(band, \"slope\")\n    if ((view.type === \"highpass\" || view.type === \"lowpass\") && props.params.some(p => p.path === path)) {\n      const binding = faceParamBinding(props, path)\n      return binding.options?.[Math.round(binding.value)] ?? `Q ${(view.q ?? 0).toFixed(2)}`\n    }\n    return `Q ${(view.q ?? readModuleFaceValue(props, bandPath(band, \"q\"))).toFixed(2)}`\n  }\n  const [selectedBand, setSelectedBand] = useState(Math.min(2, bandCount - 1))\n  const edit = useFaceParamEditGesture(props)\n  const hostRate = props.transport?.sampleRate\n  const rateKnown = typeof hostRate === \"number\" && Number.isFinite(hostRate) && hostRate > 0\n  const sampleRate = rateKnown ? hostRate : 48000\n  const frequencyDescriptor = faceParamBinding(props, bandPath(0, \"freq\"))\n  const minFrequency = frequencyDescriptor.min\n  const maxFrequency = frequencyDescriptor.max\n  const frequencyLimit = Math.min(maxFrequency, frequencyLimitRatio * sampleRate)\n  const analysis = useEqAnalysis(props, sampleRate, minFrequency, maxFrequency)\n  const advanced = props.params.some(p => p.path === bandPath(0, \"channel\"))\n  const powered = isFaceModulePowered(props)\n  const bands = useMemo<EQEditorBand[]>(() => Array.from({ length: bandCount }, (_, band) => {\n    const type = Math.round(readModuleFaceValue(props, bandPath(band, \"type\")))\n    const freq = faceParamBinding(props, bandPath(band, \"freq\"))\n    const gain = faceParamBinding(props, bandPath(band, \"gain\"))\n    const q = faceParamBinding(props, bandPath(band, \"q\"))\n    return { type: bandTypeToEditor(type), freq: freq.value, gain: gain.value, q: q.value, bypassed: readModuleFaceValue(props, bandPath(band, \"enabled\")) < 0.5, color: BAND_COLORS[band], label: bandLabel(band), freqRange: { min: freq.min, max: freq.max }, gainRange: bandUsesGain(type) ? { min: gain.min, max: gain.max } : null, qRange: { min: q.min, max: q.max }, sections: props.params.some(p => p.path === bandPath(band, \"slope\")) ? readModuleFaceValue(props, bandPath(band, \"slope\")) + 1 : 1 }\n  }), [props.params, props.values, bandCount, bandPath, bandTypeToEditor, bandUsesGain, bandRoles])\n  const onBandChange = useCallback((index: number, patch: BandPatch) => {\n    for (const field of [\"freq\", \"gain\", \"q\"] as const) if (patch[field] !== undefined) edit.change(bandPath(index, field), patch[field]!)\n  }, [edit, bandPath])\n  const onBandEditEnd = useCallback((index: number, band: EQEditorBand) => {\n    for (const field of [\"freq\", \"gain\", \"q\"] as const) edit.end(bandPath(index, field), band[field] ?? readModuleFaceValue(props, bandPath(index, field)))\n  }, [edit, bandPath, props])\n  const selected = bands[selectedBand]!\n  const selectedType = Math.round(readModuleFaceValue(props, bandPath(selectedBand, \"type\")))\n  const slopePath = bandPath(selectedBand, \"slope\")\n  const hasSlope = props.params.some(p => p.path === slopePath)\n  const isCut = selected.type === \"highpass\" || selected.type === \"lowpass\"\n  const resetBand = () => { for (const field of [\"enabled\", \"type\", \"freq\", \"gain\", \"q\", \"slope\", \"channel\", \"dynamic\", \"threshold\", \"range\", \"attack\", \"release\"]) { const path = bandPath(selectedBand, field); if (!props.params.some(p => p.path === path)) continue; const b = faceParamBinding(props, path); edit.change(path, b.defaultValue); edit.end(path, b.defaultValue) } }\n\n  return <main className=\"peq-face\" data-advanced={advanced} data-producer-eq-panel=\"true\" data-module-powered={powered}>\n    <FaceModuleNavbar face={props} brand={title} showStatus={false} fallbackPresets={fallbackPresets} presetVariant=\"quiet\" presetPaging=\"off\" presetSearch unstyled classNames={{ root: \"peq-head\", brand: \"peq-brand\", name: \"peq-title\", preset: \"peq-preset\", actions: \"peq-output\" }} actions={<ParamNumber face={props} path={outputGainPath} label=\"Output\" unit=\"dB\" disabled={!powered} />} />\n    <div className=\"peq-face__curve\">\n      <EQEditor plotBackground={analysis.background} bands={bands} onBandChange={onBandChange} onBandEditEnd={onBandEditEnd} selectedBand={selectedBand} onBandSelect={setSelectedBand} handleLabels=\"number\" showBypassedHandles showAxisLabels adaptiveGainRange disabled={!powered} mode=\"parametric\" height=\"var(--peq-plot-height, clamp(180px, calc(100vh - 330px), 440px))\" color=\"#eef2f5\" fill={false} minFrequency={minFrequency} maxFrequency={maxFrequency} sampleRate={sampleRate} frequencyLimit={frequencyLimit} decibelRange={18} showBands showGrid showLabels showGuides aria-label=\"EQ shape response and band controls\" />\n    </div>\n    <section className=\"peq-inspector\" aria-label={`Band ${selectedBand + 1} detail`} style={{ \"--peq-band-accent\": BAND_COLORS[selectedBand] } as CSSProperties}>\n      <div className=\"peq-selected-title\"><span className=\"peq-number\">{selectedBand + 1}</span><strong>{bandLabel(selectedBand)}</strong><BandEnable face={props} band={selectedBand} path={bandPath(selectedBand, \"enabled\")} /></div>\n      <ParamSelect face={props} path={bandPath(selectedBand, \"type\")} label=\"Type\" disabled={!powered} />\n      <ParamNumber face={props} path={bandPath(selectedBand, \"freq\")} label=\"Frequency\" unit=\"Hz\" disabled={!powered} />\n      <ParamNumber face={props} path={bandPath(selectedBand, \"gain\")} label=\"Gain\" unit=\"dB\" disabled={!powered || !bandUsesGain(selectedType)} />\n      <ParamNumber face={props} path={bandPath(selectedBand, \"q\")} label=\"Q\" disabled={!powered} />\n      {hasSlope && isCut && <ParamSelect face={props} path={slopePath} label=\"Slope\" disabled={!powered} />}\n      {props.params.some(p => p.path === bandPath(selectedBand, \"channel\")) && <ParamSelect face={props} path={bandPath(selectedBand, \"channel\")} label=\"Channel\" disabled={!powered} />}\n      {props.params.some(p => p.path === bandPath(selectedBand, \"dynamic\")) && <ParamSelect face={props} path={bandPath(selectedBand, \"dynamic\")} label=\"Dynamics\" disabled={!powered || selected.type !== \"peaking\"} />}\n      {props.params.some(p => p.path === bandPath(selectedBand, \"dynamic\")) && readModuleFaceValue(props, bandPath(selectedBand, \"dynamic\")) >= 0.5 && selected.type === \"peaking\" && <>{[ [\"threshold\", \"Threshold\", \"dBFS\"], [\"range\", \"Downward range\", \"dB\"], [\"attack\", \"Attack\", \"ms\"], [\"release\", \"Release\", \"ms\"] ].map(([field, label, unit]) => <ParamNumber key={field} face={props} path={bandPath(selectedBand, field!)} label={label!} unit={unit} disabled={!powered} />)}</>}\n      </section>\n      <div className=\"peq-tools\">\n      {auditionBandPath && props.params.some(p => p.path === auditionBandPath) && <BandListen face={props} band={selectedBand} powered={powered} path={auditionBandPath} />}\n      <ConsoleButton size=\"sm\" variant=\"ghost\" className=\"peq-reset\" onClick={resetBand} disabled={!powered}>Reset band</ConsoleButton>\n      {analysis.controls}\n      </div>\n    {advanced && readModuleFaceValue(props, bandPath(selectedBand, \"dynamic\")) >= 0.5 && <BandDynamicsStatus face={props} band={selectedBand} bandPath={bandPath} />}\n    <div className=\"peq-summary\" aria-label=\"Band overview\" style={{ \"--peq-band-count\": bandCount } as CSSProperties}>\n      {bands.map((band, index) => <section key={index} className=\"peq-summary-band\" data-selected={selectedBand === index} data-enabled={!band.bypassed} style={{ \"--peq-band-accent\": BAND_COLORS[index] } as CSSProperties}>\n        <div className=\"peq-summary-head\"><Button type=\"button\" className=\"peq-band-select\" aria-pressed={selectedBand === index} onClick={() => setSelectedBand(index)}><span className=\"peq-number\">{index + 1}</span><span>{bandLabel(index)}</span></Button><BandEnable face={props} band={index} path={bandPath(index, \"enabled\")} /></div>\n        <Button type=\"button\" className=\"peq-band-readout\" aria-label={`Select band ${index + 1}: ${band.type}, ${frequencyText(band.freq)}`} onClick={() => setSelectedBand(index)}><span><span className=\"peq-readout-value\">{frequencyText(band.freq)}</span></span><span><span className=\"peq-readout-value\">{bandSummary(index, band)}</span></span></Button>\n      </section>)}\n    </div>\n\n  </main>\n}\n",
      "type": "registry:component",
      "target": "@ui/producer-eq-panel.tsx"
    },
    {
      "path": "components/ui/producer-eq-panel.css",
      "content": "/**\n * Source: https://maru.audio/ui-kit/producer-eq-panel\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 */\n/* Shared compact EQ skin: surface, text and accent are the only palette inputs. */\n.peq-face {\n  --uikit-surface: var(--peq-surface, #15191e);\n  --uikit-text: var(--peq-text, #eef2f5);\n  --uikit-accent: var(--peq-accent, #2992ff);\n  --uikit-accent-2: var(--uikit-accent);\n  --uikit-cap: color-mix(in srgb, var(--uikit-surface) 94%, white);\n  --uikit-text-muted: color-mix(in srgb, var(--uikit-text) 68%, var(--uikit-surface));\n  --uikit-border: color-mix(in srgb, var(--uikit-text) 16%, var(--uikit-surface));\n  --uikit-track: color-mix(in srgb, var(--uikit-text) 21%, var(--uikit-surface));\n  --uikit-focus: var(--uikit-accent);\n  --uikit-radius: 5px;\n  --uikit-font: Inter, system-ui, sans-serif;\n  --uikit-font-mono: var(--uikit-font);\n  --uikit-control-h-lg: 32px;\n  --uikit-eq-axis-font-size: 12px;\n  --peq-plot-height: clamp(150px, calc(100vh - 345px), 430px);\n  box-sizing: border-box;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  min-height: 100%;\n  padding: 14px;\n  color: var(--uikit-text);\n  background: var(--uikit-surface);\n  font: 14px/1.4 var(--uikit-font);\n}\n.peq-face *, .peq-face *::before, .peq-face *::after { box-sizing: border-box; }\n.peq-face button { cursor: pointer; }\n.peq-face button:disabled { cursor: default; opacity: .5; }\n.peq-face button:focus-visible { outline: 2px solid var(--uikit-focus); outline-offset: 3px; }\n.peq-face .peq-head { display: flex; align-items: center; flex-wrap: nowrap; gap: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--uikit-border); }\n.peq-face .peq-brand { flex: 0 0 auto; min-width: 0; }\n.peq-face .peq-title { font-size: clamp(20px, 2.3vw, 28px); font-weight: 650; line-height: 1.15; letter-spacing: -.025em; }\n.peq-face .peq-preset { margin-left: auto; flex: 0 1 320px; min-width: 120px; }\n.peq-face [data-module-navbar-trailing] { display: flex; align-items: center; gap: 10px; margin-left: auto; }\n.peq-face .peq-output { width: 90px; }\n.peq-face .peq-output .peq-field { min-width: 0; }\n.peq-face__curve { --uikit-radius: 0px; position: relative; min-width: 0; }\n.peq-inspector { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; min-height: 70px; padding: 10px; border: 1px solid var(--uikit-border); border-radius: 5px; }\n.peq-selected-title { display: flex; align-items: center; gap: 10px; margin-right: auto; }\n.peq-selected-title strong { font-size: 18px; font-weight: 600; white-space: nowrap; }\n.peq-number { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 24px; width: 24px; height: 24px; color: #101419; background: var(--peq-band-accent); border-radius: 50%; font-weight: 700; font-size: 14px; line-height: 1; }\n.peq-selected-title > .peq-number { width: 34px; height: 34px; flex-basis: 34px; font-size: 20px; }\n.peq-field { display: flex; flex-direction: column; gap: 4px; min-width: 75px; flex: 1 1 90px; max-width: 150px; }\n.peq-field > span { color: var(--uikit-text-muted); font-size: 11px; }\n.peq-field input { font-variant-numeric: tabular-nums; }\n.peq-field--select { align-self: flex-end; }\n.peq-enable { display: inline-flex; align-items: center; flex: 0 0 auto; }\n.peq-enable [data-console-switch][data-checked] [data-console-switch-track] { border-color: var(--uikit-accent); background: var(--uikit-accent); }\n.peq-reset { white-space: nowrap; }\n.peq-summary { display: grid; grid-template-columns: repeat(var(--peq-band-count, 6), minmax(0, 1fr)); gap: 6px; }\n.peq-summary-band { min-width: 0; padding: 8px; border: 1px solid var(--uikit-border); border-radius: 5px; }\n.peq-summary-band[data-selected=\"true\"] { border-color: var(--peq-band-accent); box-shadow: inset 0 0 0 1px var(--peq-band-accent); }\n.peq-summary-band[data-enabled=\"false\"] .peq-band-readout { color: var(--uikit-text-muted); }\n.peq-summary-head { display: flex; align-items: center; justify-content: space-between; gap: 4px; }\n.peq-band-select, .peq-band-readout { border: 0; background: transparent; color: var(--uikit-text); text-align: left; padding: 0; font: inherit; }\n.peq-band-select { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 12px; }\n.peq-band-readout { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 8px; width: 100%; margin-top: 6px; font-size: 12px; font-variant-numeric: tabular-nums; }\n.peq-readout-value { white-space: nowrap; }\n.peq-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }\n.peq-analysis { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-left: auto; font-size: 11px; color: var(--uikit-text-muted); }\n.peq-analysis button[aria-pressed=\"true\"] { color: var(--uikit-accent); }\n.peq-dynamics-status { color: var(--uikit-text-muted); font-size: 11px; font-variant-numeric: tabular-nums; }\n.peq-face .peq-select-trigger { font-family: var(--uikit-font) !important; font-size: 14px !important; font-weight: 400 !important; letter-spacing: normal !important; text-transform: none !important; }\n/* EQEditor owns hit geometry; numbered labels provide the product's visual nodes. */\n.peq-face__curve svg[role=\"img\"] { border: 0 !important; border-radius: 0 !important; }\n.peq-face__curve [data-eq-marker=\"outline\"], .peq-face__curve [data-eq-marker=\"fill\"] { opacity: 0; }\n.peq-face__curve [data-eq-band-label=\"number\"] { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 1.5px solid var(--uikit-eq-band-color); border-radius: 50%; background: var(--uikit-eq-band-color); color: #101419 !important; font: 700 14px/1 var(--uikit-font) !important; }\n.peq-face__curve [data-eq-band-label=\"number\"][data-selected=\"true\"] { outline: 1.5px solid var(--uikit-eq-band-color); outline-offset: 3px; box-shadow: inset 0 0 0 1.5px #101419; }\n.peq-face__curve [data-eq-band-label=\"number\"][data-bypassed=\"true\"] { background: var(--uikit-surface); color: var(--uikit-eq-band-color) !important; opacity: .65; border-style: dashed; }\n.peq-face__curve [data-eq-curve=\"band\"], .peq-face__curve [data-eq-curve=\"sum\"] { stroke-width: 2; vector-effect: non-scaling-stroke; }\n@media (max-width: 1200px) { .peq-face[data-advanced=\"true\"] .peq-summary { grid-template-columns: repeat(4, minmax(0, 1fr)); } }\n@media (max-width: 800px) {\n  .peq-face { --peq-plot-height: clamp(160px, calc(100vh - 390px), 300px); }\n  .peq-face .peq-head { flex-wrap: wrap; gap: 10px; }\n  .peq-face .peq-preset { order: 2; flex: 1 1 100%; margin-left: 0; }\n  .peq-summary, .peq-face[data-advanced=\"true\"] .peq-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }\n  .peq-selected-title { flex-basis: 100%; }\n  .peq-inspector .peq-field { flex: 1 1 90px; max-width: none; }\n}\n@media (max-width: 460px) {\n  .peq-face { padding: 10px; --peq-plot-height: 200px; --uikit-eq-axis-font-size: 10px; }\n  .peq-face .peq-title { font-size: 20px; }\n  .peq-face[data-advanced=\"true\"] .peq-title { font-size: 18px; }\n  .peq-face .peq-output { width: 80px; }\n  .peq-summary, .peq-face[data-advanced=\"true\"] .peq-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n  .peq-inspector .peq-field { min-width: 100px; flex-basis: calc(50% - 12px); }\n  .peq-analysis { margin-left: 0; }\n}\n",
      "type": "registry:file",
      "target": "@ui/producer-eq-panel.css"
    },
    {
      "path": "components/ui/use-eq-analysis.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/producer-eq-panel\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, useState } from \"react\"\nimport { ConsoleToggle } from \"@/components/ui/console-button\"\nimport type { AudioModuleFaceProps } from \"maruaudio/ui/shared/maru/face-preview\"\nimport { analyzeEqScope, eqSpectrumPath } from \"maruaudio/ui/shared/eq-analysis\"\n\ntype Tap = \"pre\" | \"post\"\ninterface ScopeFrame { path: string; peakDb: number; clipped: boolean }\n\n/** Analyze only PCM supplied by the host; missing taps never acquire substitute data. */\nexport function useEqAnalysis(face: AudioModuleFaceProps, sampleRate: number, minFrequency = 20, maxFrequency = 20000) {\n  const [tap, setTap] = useState<Tap>(\"post\")\n  const [enabled, setEnabled] = useState(true)\n  const [frame, setFrame] = useState<ScopeFrame | null>(null)\n  const reader = tap === \"pre\" ? face.readStereoInputScope : face.readStereoScope\n  const active = face.live !== false && face.power?.powered !== false\n  useEffect(() => {\n    setFrame(null)\n    if (!enabled || !reader || !active) return\n    const left = new Float32Array(1024)\n    const right = new Float32Array(1024)\n    const update = () => {\n      if (typeof document !== \"undefined\" && document.hidden) return\n      if (!reader(left, right)) { setFrame(null); return }\n      const analysis = analyzeEqScope(left, right, sampleRate)\n      setFrame({ path: eqSpectrumPath(analysis.db, analysis.fftSize, sampleRate, minFrequency, maxFrequency), peakDb: analysis.peakDb, clipped: analysis.clipped })\n    }\n    update()\n    const timer = setInterval(update, 125)\n    return () => clearInterval(timer)\n  }, [reader, enabled, active, sampleRate, minFrequency, maxFrequency])\n\n  const background = frame ? <path d={frame.path} fill=\"none\" stroke=\"var(--uikit-text-muted, #94a3b8)\" strokeWidth=\"0.6\" opacity=\"0.45\" vectorEffect=\"non-scaling-stroke\" aria-hidden=\"true\" /> : null\n  const controls = <div className=\"peq-analysis\" aria-label=\"Spectrum analyzer\">\n    <ConsoleToggle size=\"sm\" label=\"Spectrum\" pressed={enabled} onPressedChange={setEnabled} />\n    <ConsoleToggle size=\"sm\" label=\"Pre\" pressed={tap === \"pre\"} onPressedChange={() => setTap(\"pre\")} />\n    <ConsoleToggle size=\"sm\" label=\"Post\" pressed={tap === \"post\"} onPressedChange={() => setTap(\"post\")} />\n    {enabled && <span aria-description={`Spectrum −90…0 dBFS · ${(sampleRate / 1024).toFixed(1)} Hz bins. ${tap === \"pre\" ? \"Input\" : \"Output\"} peak.`}>{frame ? `${frame.peakDb <= -120 ? \"−∞\" : frame.peakDb.toFixed(1)} dBFS${frame.clipped ? \" · Clip\" : \"\"}` : \"Unavailable\"}</span>}\n\n  </div>\n  return { background, controls }\n}\n",
      "type": "registry:hook",
      "target": "@ui/use-eq-analysis.tsx"
    }
  ]
}
