{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-output-level-meter",
  "type": "registry:ui",
  "title": "Output Level Meter",
  "description": "A module's output meter with its gain control on it: pixel-aligned LED bars, one per channel (L/R when the host measures them, one merged bar otherwise), hardware green/amber/red zones, a peak-hold segment and a latched clip light, with a triangle under the bars that sets the linear output gain on the meter's own dB scale. Levels paint straight to the DOM, so a 30 Hz meter feed never re-renders the bars.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-gain-reduction-meter.json"
  ],
  "files": [
    {
      "path": "components/ui/output-level-meter.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/output-level-meter\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/**\n * OutputLevelMeter — a module's output meter with its gain control on it.\n *\n * The compact horizontal meter a plugin header carries: one LED bar per\n * channel (two thin bars for stereo, one when only a merged level is known),\n * a peak-hold segment, a latched clip light, and — when `gain` is given — a\n * triangle under the bars that sets the output gain on the meter's own dB\n * scale, so the triangle marks the ceiling the bars sit under.\n *\n * Built to read crisply over anything, artwork included:\n *\n *   - Pixel-aligned. `length` is snapped DOWN to a whole number of integer-\n *     width segments and gaps, and the bar thickness to whole pixels, so every\n *     LED edge lands on a pixel. No blur, glow, gradient or filter anywhere.\n *   - Hardware colour. Each segment's colour belongs to its place on the scale\n *     (green, amber from -18 dB, red from -6 dB; `zones` moves them, and the\n *     `--uikit-meter-low/-mid/-high` tokens recolour them). Unlit segments show\n *     their colour dimmed, so the scale is legible at silence.\n *   - Ballistics. The bar attacks instantly and releases at `releaseDbPerSecond`;\n *     the hold segment sits `holdMs` then falls. A clip (any channel flagged\n *     `clipped`, or at full scale) latches the clip light for `clipHoldMs`;\n *     a click on it clears it.\n *\n * Levels change at the host's meter rate, so they never re-render the bars: a\n * new `levels` value is pushed into a small engine that paints the lit width,\n * the hold segment and the clip light straight onto the DOM, and animates the\n * release on `requestAnimationFrame` only while something is still falling.\n * The bars themselves are a memoised body that renders once per layout.\n *\n * Feed it the face contract's `AudioModuleFaceLevel` as-is: `{ peak, rms,\n * clipped, channels? }`. With `channels` (two or more), one bar per channel;\n * without, one bar showing the merged reading. Once a host has reported\n * channels the meter keeps that many bars, so a silent frame without the list\n * does not change its shape.\n *\n * The gain is linear (1 = unity), as module `level` parameters are. Drag the\n * triangle or anywhere on the meter; arrow keys move it 1 dB (Shift 0.1 dB);\n * Home / End go to the range ends; double-click returns to `defaultValue`.\n * Every change is bracketed by `onChangeStart` / `onChangeEnd`, the host's\n * edit-gesture pair.\n *\n * The maths is exported as pure functions — `outputMeterLayout`,\n * `meterSegmentsLit`, `advanceMeterChannel`, `gainToMeterUnit`,\n * `meterUnitToGain`, `stepGainDb` — testable in Node with no DOM.\n */\n\nimport { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\"\nimport type { CSSProperties, KeyboardEvent, PointerEvent } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  LEVEL_ZONE_DEFAULTS,\n  levelZone,\n  type LevelZone,\n  type LevelZoneThresholds,\n} from \"@/components/ui/gain-reduction-meter\"\n\n// ---------------------------------------------------------------------------\n// Pure core — no React, no DOM.\n// ---------------------------------------------------------------------------\n\n/** One channel's reading, linear amplitude (1 is full scale). */\nexport interface OutputMeterChannel {\n  readonly peak: number\n  readonly rms?: number\n  readonly clipped?: boolean\n}\n\n/** A bus reading: the merged value, and optionally each channel. Structurally\n * the face contract's `AudioModuleFaceLevel`. */\nexport interface OutputMeterLevel extends OutputMeterChannel {\n  readonly channels?: readonly OutputMeterChannel[]\n}\n\nexport interface OutputMeterLayout {\n  /** Width of one LED segment, whole pixels. */\n  readonly segmentWidth: number\n  /** Segment plus gap: the x of segment `i` is `i * pitch`. */\n  readonly pitch: number\n  /** The bars' real length after snapping, whole pixels. */\n  readonly trackLength: number\n  /** Thickness of one channel's bar, whole pixels. */\n  readonly barThickness: number\n  /** Total height of the stacked bars, whole pixels. */\n  readonly barsHeight: number\n}\n\n/**\n * Snap the requested size to whole pixels: `segments` integer-width LEDs with\n * `gap`-pixel gaps (the length is rounded down to fit them), and `channels`\n * bars of equal integer thickness, one pixel apart, inside `height`.\n */\nexport function outputMeterLayout(\n  length: number,\n  height: number,\n  segments: number,\n  gap: number,\n  channels: number,\n): OutputMeterLayout {\n  const count = Math.max(1, Math.floor(segments))\n  const space = Math.max(0, Math.floor(gap))\n  const segmentWidth = Math.max(1, Math.floor((Math.floor(length) - (count - 1) * space) / count))\n  const bars = Math.max(1, Math.floor(channels))\n  const barGap = bars > 1 ? 1 : 0\n  const barThickness = Math.max(1, Math.floor((Math.floor(height) - (bars - 1) * barGap) / bars))\n  return {\n    segmentWidth,\n    pitch: segmentWidth + space,\n    trackLength: count * segmentWidth + (count - 1) * space,\n    barThickness,\n    barsHeight: bars * barThickness + (bars - 1) * barGap,\n  }\n}\n\n/** Linear amplitude → dB, floored so silence is a number. */\nexport function amplitudeToMeterDb(amplitude: number, floorDb: number): number {\n  if (!(amplitude > 0)) return floorDb\n  return Math.max(floorDb, 20 * Math.log10(amplitude))\n}\n\n/**\n * How many segments a level lights, hardware-style: segment `i` lights once\n * the level passes its lower edge, so the first LED comes on just above the\n * floor and the last one only near the ceiling.\n */\nexport function meterSegmentsLit(db: number, segments: number, minDb: number, maxDb: number): number {\n  if (maxDb <= minDb || !(db > minDb)) return 0\n  const unit = Math.min(1, (db - minDb) / (maxDb - minDb))\n  return Math.min(segments, Math.ceil(unit * segments - 1e-9))\n}\n\n/** The colour zone of each segment, by its centre on the scale. */\nexport function meterSegmentZones(\n  segments: number,\n  minDb: number,\n  maxDb: number,\n  zones: LevelZoneThresholds = LEVEL_ZONE_DEFAULTS,\n): readonly LevelZone[] {\n  return Array.from({ length: segments }, (_, i) =>\n    levelZone(minDb + (maxDb - minDb) * ((i + 0.5) / segments), zones),\n  )\n}\n\n/** One bar's animation state, in dB. Mutated in place by `advanceMeterChannel`. */\nexport interface MeterChannelState {\n  db: number\n  holdDb: number\n  holdUntil: number\n  at: number\n}\n\nexport interface MeterBallistics {\n  readonly floorDb: number\n  /** Bar fall rate. */\n  readonly releaseDbPerSecond: number\n  /** How long the hold segment stays before falling. */\n  readonly holdMs: number\n  /** Hold segment fall rate once its hold time is over. */\n  readonly holdReleaseDbPerSecond: number\n}\n\nexport function createMeterChannelState(floorDb: number): MeterChannelState {\n  return { db: floorDb, holdDb: floorDb, holdUntil: 0, at: Number.NaN }\n}\n\n/**\n * Move one bar to time `now` (ms), applying a new reading if there is one:\n * instant attack, linear-in-dB release, a hold that waits then falls and never\n * sits under the bar. Returns whether the bar has settled at the floor (no\n * further frames are needed).\n */\nexport function advanceMeterChannel(\n  state: MeterChannelState,\n  now: number,\n  ballistics: MeterBallistics,\n  inputDb?: number,\n): boolean {\n  const { floorDb } = ballistics\n  const dt = Number.isFinite(state.at) ? Math.max(0, (now - state.at) / 1000) : 0\n  state.at = now\n  state.db = Math.max(floorDb, state.db - ballistics.releaseDbPerSecond * dt)\n  if (now > state.holdUntil)\n    state.holdDb = Math.max(floorDb, state.holdDb - ballistics.holdReleaseDbPerSecond * dt)\n  if (inputDb !== undefined && Number.isFinite(inputDb)) {\n    if (inputDb > state.db) state.db = inputDb\n    if (inputDb >= state.holdDb) {\n      state.holdDb = inputDb\n      state.holdUntil = now + ballistics.holdMs\n    }\n  }\n  if (state.holdDb < state.db) state.holdDb = state.db\n  return state.db <= floorDb && state.holdDb <= floorDb\n}\n\n/** A linear gain as a position along the scale, 0..1; silence is 0. */\nexport function gainToMeterUnit(gain: number, minDb: number, maxDb: number): number {\n  if (!(gain > 0) || maxDb <= minDb) return 0\n  const db = 20 * Math.log10(gain)\n  return Math.min(1, Math.max(0, (db - minDb) / (maxDb - minDb)))\n}\n\n/** A position along the scale as a linear gain; the far left is silence. */\nexport function meterUnitToGain(unit: number, minDb: number, maxDb: number): number {\n  if (!(unit > 0)) return 0\n  return 10 ** ((minDb + Math.min(1, unit) * (maxDb - minDb)) / 20)\n}\n\n/**\n * Where a pointer is along the scale, 0..1. `rectLeft`/`rectWidth` are the\n * plate's on-screen box (`getBoundingClientRect`, already scaled by any CSS\n * `zoom` or transform around the meter); `layoutWidth` is its unscaled\n * `offsetWidth`, and `inset` and `trackLength` are unscaled layout pixels.\n * The pointer is brought back to layout pixels first, so the handle lands\n * under the pointer at any face scale.\n */\nexport function meterPointerUnit(\n  clientX: number,\n  rectLeft: number,\n  rectWidth: number,\n  layoutWidth: number,\n  inset: number,\n  trackLength: number\n): number {\n  if (!(trackLength > 0)) return 0\n  const scale = layoutWidth > 0 && rectWidth > 0 ? rectWidth / layoutWidth : 1\n  const x = (clientX - rectLeft) / scale - inset\n  return Math.min(1, Math.max(0, x / trackLength))\n}\n\n/**\n * One keyboard step of a linear gain, in dB. Stepping down past the floor of\n * the scale reaches silence (0); stepping up from silence starts at the floor.\n */\nexport function stepGainDb(gain: number, stepDb: number, floorDb: number): number {\n  const db = gain > 0 ? 20 * Math.log10(gain) : floorDb - Math.abs(stepDb)\n  const next = db + stepDb\n  if (stepDb < 0 && next <= floorDb) return 0\n  return 10 ** (next / 20)\n}\n\n/** A linear gain as signed decibels, one decimal: \"−3.5 dB\", \"−∞ dB\". */\nexport function formatGainDb(gain: number): string {\n  if (!(gain > 0)) return \"−∞ dB\"\n  const db = Math.round(20 * Math.log10(gain) * 10) / 10\n  return `${db > 0 ? \"+\" : db < 0 ? \"−\" : \"\"}${Math.abs(db)} dB`\n}\n\n// ---------------------------------------------------------------------------\n// React\n// ---------------------------------------------------------------------------\n\nexport interface OutputGainControl {\n  /** Linear gain, 1 = unity. */\n  readonly value: number\n  readonly min: number\n  readonly max: number\n  /** Double-click returns here. */\n  readonly defaultValue: number\n  readonly onChange: (value: number) => void\n  /** Start of an edit gesture (pointer down, or a keyboard step). */\n  readonly onChangeStart?: (() => void) | undefined\n  /** End of an edit gesture, with the final value. */\n  readonly onChangeEnd?: ((value: number) => void) | undefined\n  /** The handle's accessible name. Default \"Volume\". */\n  readonly label?: string\n  /** Written as `data-audio-param-path` on the handle, for CC and automation layers. */\n  readonly paramPath?: string\n}\n\nexport interface OutputLevelMeterProps {\n  /** The output reading; absent draws an unlit meter (no meters from this host). */\n  levels?: OutputMeterLevel | undefined\n  /** Which reading drives the bars. Default `peak`. */\n  reading?: \"peak\" | \"rms\"\n  /** The gain this meter's output is under. Absent: a meter alone. */\n  gain?: OutputGainControl | undefined\n  /** Bottom of the scale in dB. */\n  minDecibels?: number\n  /** Top of the scale in dB. */\n  maxDecibels?: number\n  /** Requested bar length in px; snapped down to whole segments. */\n  length?: number\n  /** Height of the stacked bars in px; snapped to whole-pixel bars. */\n  height?: number\n  /** LED count along the scale. */\n  segments?: number\n  /** Gap between LEDs, px. */\n  segmentGap?: number\n  /** Amber and red boundaries, dB. */\n  zones?: LevelZoneThresholds\n  releaseDbPerSecond?: number\n  holdMs?: number\n  holdReleaseDbPerSecond?: number\n  /** How long a clip keeps the clip light on. */\n  clipHoldMs?: number\n  /** The meter's accessible name. Default \"Output level\". */\n  \"aria-label\"?: string\n  className?: string\n  style?: CSSProperties\n}\n\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect\n\nconst ZONE_COLOR: Record<LevelZone, string> = {\n  low: \"var(--uikit-meter-low, #22c55e)\",\n  mid: \"var(--uikit-meter-mid, #f59e0b)\",\n  high: \"var(--uikit-meter-high, #ef4444)\",\n}\nconst PLATE_PADDING = 2\nconst PLATE_BORDER = 1\nconst CLIP_WIDTH = 3\nconst CLIP_GAP = 2\nconst HANDLE_WIDTH = 9\nconst HANDLE_HEIGHT = 6\nconst UNLIT_OPACITY = 0.2\n\nconst now = () => (typeof performance !== \"undefined\" ? performance.now() : Date.now())\n\n/** What the engine paints into: per-bar elements, plus the clip light and the\n * meter's ARIA value. */\ninterface MeterTargets {\n  lit: (HTMLElement | null)[]\n  hold: (HTMLElement | null)[]\n  clip: HTMLElement | null\n  meter: HTMLElement | null\n}\n\ninterface MeterConfig {\n  readonly layout: OutputMeterLayout\n  readonly segments: number\n  readonly gap: number\n  readonly minDb: number\n  readonly maxDb: number\n  readonly colors: readonly string[]\n  readonly ballistics: MeterBallistics\n  readonly clipHoldMs: number\n}\n\n/** The imperative half: per-bar state, the paint, and the release loop. */\nclass MeterEngine {\n  readonly targets: MeterTargets = { lit: [], hold: [], clip: null, meter: null }\n  config: MeterConfig\n  private states: MeterChannelState[] = []\n  private painted: { lit: number; hold: number }[] = []\n  private clipUntil = 0\n  /** null: unknown, paint on the next frame. */\n  private clipShown: boolean | null = null\n  private ariaDb = Number.NaN\n  private frame = 0\n\n  constructor(config: MeterConfig) {\n    this.config = config\n  }\n\n  private ensure(count: number) {\n    while (this.states.length < count) {\n      this.states.push(createMeterChannelState(this.config.ballistics.floorDb))\n      this.painted.push({ lit: -1, hold: -1 })\n    }\n    this.states.length = count\n    this.painted.length = count\n  }\n\n  push(values: readonly number[], clipped: boolean, count: number) {\n    this.ensure(count)\n    const at = now()\n    const { floorDb } = this.config.ballistics\n    for (let i = 0; i < count; i += 1) {\n      const amplitude = values[i] ?? values[0] ?? 0\n      advanceMeterChannel(this.states[i]!, at, this.config.ballistics, amplitudeToMeterDb(amplitude, floorDb))\n    }\n    if (clipped) this.clipUntil = at + this.config.clipHoldMs\n    this.paint(at)\n  }\n\n  clearClip() {\n    this.clipUntil = 0\n    this.paint(now())\n  }\n\n  /** Force a full repaint, e.g. after the body re-rendered new elements. */\n  invalidate() {\n    for (const each of this.painted) {\n      each.lit = -1\n      each.hold = -1\n    }\n    this.clipShown = null\n    this.ariaDb = Number.NaN\n    this.paint(now())\n  }\n\n  private tick = () => {\n    this.frame = 0\n    const at = now()\n    for (const state of this.states) advanceMeterChannel(state, at, this.config.ballistics)\n    this.paint(at)\n  }\n\n  private paint(at: number) {\n    const { layout, segments, gap, minDb, maxDb, colors, ballistics } = this.config\n    let settled = true\n    let loudest = ballistics.floorDb\n    for (let i = 0; i < this.states.length; i += 1) {\n      const state = this.states[i]!\n      if (state.db > ballistics.floorDb || state.holdDb > ballistics.floorDb) settled = false\n      loudest = Math.max(loudest, state.db)\n      const lit = meterSegmentsLit(state.db, segments, minDb, maxDb)\n      const held = meterSegmentsLit(state.holdDb, segments, minDb, maxDb)\n      const hold = held > lit ? held - 1 : -1\n      const painted = this.painted[i]!\n      const litElement = this.targets.lit[i]\n      if (litElement && painted.lit !== lit) {\n        litElement.style.width = `${lit === 0 ? 0 : lit * layout.pitch - gap}px`\n        painted.lit = lit\n      }\n      const holdElement = this.targets.hold[i]\n      if (holdElement && painted.hold !== hold) {\n        if (hold < 0) holdElement.style.display = \"none\"\n        else {\n          holdElement.style.display = \"block\"\n          holdElement.style.left = `${hold * layout.pitch}px`\n          holdElement.style.background = colors[hold] ?? ZONE_COLOR.low\n        }\n        painted.hold = hold\n      }\n    }\n    const clip = at < this.clipUntil\n    if (clip) settled = false\n    if (this.targets.clip && clip !== this.clipShown) {\n      this.targets.clip.style.opacity = clip ? \"1\" : String(UNLIT_OPACITY)\n      this.targets.clip.dataset.clipped = clip ? \"true\" : \"false\"\n      this.clipShown = clip\n    }\n    const ariaDb = Math.round(loudest)\n    if (this.targets.meter && ariaDb !== this.ariaDb) {\n      this.targets.meter.setAttribute(\"aria-valuenow\", String(Math.max(minDb, ariaDb)))\n      this.ariaDb = ariaDb\n    }\n    if (settled) this.stop()\n    else if (this.frame === 0 && typeof requestAnimationFrame === \"function\")\n      this.frame = requestAnimationFrame(this.tick)\n  }\n\n  stop() {\n    if (this.frame !== 0 && typeof cancelAnimationFrame === \"function\") cancelAnimationFrame(this.frame)\n    this.frame = 0\n  }\n}\n\n/** The bars and the clip light: static structure, rendered once per layout. */\nconst MeterBars = memo(function MeterBars({\n  engine,\n  channels,\n  layout,\n  colors,\n}: {\n  engine: MeterEngine\n  channels: number\n  layout: OutputMeterLayout\n  colors: readonly string[]\n}) {\n  const { segmentWidth, pitch, trackLength, barThickness, barsHeight } = layout\n  const strip = (opacity: number) =>\n    colors.map((color, index) => (\n      <span\n        key={index}\n        style={{\n          position: \"absolute\",\n          top: 0,\n          left: index * pitch,\n          width: segmentWidth,\n          height: barThickness,\n          background: color,\n          opacity,\n        }}\n      />\n    ))\n  useIsomorphicLayoutEffect(() => {\n    engine.targets.lit.length = channels\n    engine.targets.hold.length = channels\n    engine.invalidate()\n  }, [engine, channels, layout, colors])\n  return (\n    <div style={{ display: \"flex\", alignItems: \"stretch\", gap: CLIP_GAP }}>\n      <div style={{ display: \"flex\", flexDirection: \"column\", gap: channels > 1 ? 1 : 0, width: trackLength, height: barsHeight }}>\n        {Array.from({ length: channels }, (_, channel) => (\n          <div key={channel} style={{ position: \"relative\", width: trackLength, height: barThickness, flex: \"none\" }}>\n            {strip(UNLIT_OPACITY)}\n            <div\n              ref={(element) => {\n                engine.targets.lit[channel] = element\n              }}\n              style={{ position: \"absolute\", top: 0, left: 0, height: barThickness, width: 0, overflow: \"hidden\" }}\n            >\n              {strip(1)}\n            </div>\n            <span\n              ref={(element) => {\n                engine.targets.hold[channel] = element\n              }}\n              style={{ position: \"absolute\", top: 0, left: 0, width: segmentWidth, height: barThickness, display: \"none\" }}\n            />\n          </div>\n        ))}\n      </div>\n      <span\n        ref={(element) => {\n          engine.targets.clip = element\n        }}\n        data-clipped=\"false\"\n        aria-hidden=\"true\"\n        onPointerDown={(event) => {\n          event.stopPropagation()\n          engine.clearClip()\n        }}\n        style={{\n          flex: \"none\",\n          width: CLIP_WIDTH,\n          height: barsHeight,\n          background: ZONE_COLOR.high,\n          opacity: UNLIT_OPACITY,\n          cursor: \"pointer\",\n        }}\n      />\n    </div>\n  )\n})\n\n/** The triangle under the bars, at the gain's place on the scale. */\nconst GainHandle = memo(function GainHandle({\n  gain,\n  unit,\n  trackLength,\n  onKeyDown,\n}: {\n  gain: OutputGainControl\n  unit: number\n  trackLength: number\n  onKeyDown: (event: KeyboardEvent<HTMLSpanElement>) => void\n}) {\n  const [focusVisible, setFocusVisible] = useState(false)\n  const x = Math.round(unit * trackLength)\n  return (\n    <div style={{ position: \"relative\", height: HANDLE_HEIGHT + 1, width: trackLength, marginLeft: PLATE_BORDER + PLATE_PADDING }}>\n      <span\n        role=\"slider\"\n        tabIndex={0}\n        aria-label={gain.label ?? \"Volume\"}\n        aria-orientation=\"horizontal\"\n        aria-valuemin={gain.min}\n        aria-valuemax={gain.max}\n        aria-valuenow={gain.value}\n        aria-valuetext={formatGainDb(gain.value)}\n        {...(gain.paramPath === undefined ? {} : { \"data-audio-param-path\": gain.paramPath })}\n        data-uikit-output-meter-handle=\"\"\n        onKeyDown={onKeyDown}\n        onFocus={(event) => {\n          let visible = true\n          try {\n            visible = event.currentTarget.matches(\":focus-visible\")\n          } catch {\n            /* older engines: always show */\n          }\n          setFocusVisible(visible)\n        }}\n        onBlur={() => setFocusVisible(false)}\n        style={{\n          position: \"absolute\",\n          top: 1,\n          left: x - (HANDLE_WIDTH - 1) / 2,\n          width: HANDLE_WIDTH,\n          height: HANDLE_HEIGHT,\n          display: \"block\",\n          outline: \"none\",\n          color: focusVisible ? token.focus : \"var(--uikit-meter-handle, currentColor)\",\n        }}\n      >\n        <svg width={HANDLE_WIDTH} height={HANDLE_HEIGHT} viewBox={`0 0 ${HANDLE_WIDTH} ${HANDLE_HEIGHT}`} aria-hidden=\"true\" style={{ display: \"block\", overflow: \"visible\" }}>\n          <polygon\n            points={`0,${HANDLE_HEIGHT} ${HANDLE_WIDTH / 2},0 ${HANDLE_WIDTH},${HANDLE_HEIGHT}`}\n            fill=\"currentColor\"\n            stroke=\"var(--uikit-meter-handle-edge, rgba(0, 0, 0, 0.55))\"\n            strokeWidth={1}\n            strokeLinejoin=\"round\"\n            paintOrder=\"stroke\"\n          />\n        </svg>\n      </span>\n    </div>\n  )\n})\n\nexport const OutputLevelMeter = memo(function OutputLevelMeter({\n  levels,\n  reading = \"peak\",\n  gain,\n  minDecibels = -48,\n  maxDecibels = 0,\n  length = 96,\n  height = 7,\n  segments = 24,\n  segmentGap = 1,\n  zones = LEVEL_ZONE_DEFAULTS,\n  releaseDbPerSecond = 24,\n  holdMs = 1000,\n  holdReleaseDbPerSecond = 36,\n  clipHoldMs = 2000,\n  \"aria-label\": ariaLabel = \"Output level\",\n  className,\n  style,\n}: OutputLevelMeterProps) {\n  const count = Math.max(1, Math.floor(segments))\n  const reported = levels?.channels && levels.channels.length >= 2 ? levels.channels.length : 1\n  /* Sticky: a host that reported channels keeps its bars through a frame\n   * that carries only the merged value (a silent reset, typically). */\n  const [channels, setChannels] = useState(reported)\n  const shown = Math.max(channels, reported)\n  useIsomorphicLayoutEffect(() => {\n    if (reported > channels) setChannels(reported)\n  }, [reported, channels])\n\n  const layout = useMemo(\n    () => outputMeterLayout(length, height, count, segmentGap, shown),\n    [length, height, count, segmentGap, shown],\n  )\n  const amber = zones.amber\n  const red = zones.red\n  const colors = useMemo(\n    () => meterSegmentZones(count, minDecibels, maxDecibels, { amber, red }).map((zone) => ZONE_COLOR[zone]),\n    [count, minDecibels, maxDecibels, amber, red],\n  )\n  const config: MeterConfig = {\n    layout,\n    segments: count,\n    gap: Math.max(0, Math.floor(segmentGap)),\n    minDb: minDecibels,\n    maxDb: maxDecibels,\n    colors,\n    ballistics: {\n      floorDb: minDecibels,\n      releaseDbPerSecond,\n      holdMs,\n      holdReleaseDbPerSecond,\n    },\n    clipHoldMs,\n  }\n  const engineRef = useRef<MeterEngine | null>(null)\n  if (engineRef.current === null) engineRef.current = new MeterEngine(config)\n  const engine = engineRef.current\n  engine.config = config\n  useEffect(() => () => engine.stop(), [engine])\n\n  useIsomorphicLayoutEffect(() => {\n    if (levels === undefined) {\n      engine.push([0], false, shown)\n      return\n    }\n    const pick = (channel: OutputMeterChannel) => (reading === \"rms\" ? (channel.rms ?? channel.peak) : channel.peak)\n    const list = levels.channels && levels.channels.length >= 2 ? levels.channels : undefined\n    const values = list ? list.map(pick) : [pick(levels)]\n    const clipped =\n      levels.clipped === true ||\n      levels.peak >= 1 ||\n      (list?.some((channel) => channel.clipped === true || channel.peak >= 1) ?? false)\n    engine.push(values, clipped, shown)\n  }, [engine, levels, reading, shown])\n\n  // ── the gain gesture ──────────────────────────────────────────────────────\n  const barsRef = useRef<HTMLDivElement | null>(null)\n  const lastRef = useRef<number | undefined>(undefined)\n  const clamp = useCallback(\n    (value: number) => (gain === undefined ? value : Math.min(gain.max, Math.max(gain.min, value))),\n    [gain],\n  )\n  const fromPointer = useCallback(\n    (clientX: number) => {\n      const bars = barsRef.current\n      if (bars === null || gain === undefined) return undefined\n      if (layout.trackLength <= 0) return undefined\n      const rect = bars.getBoundingClientRect()\n      const unit = meterPointerUnit(clientX, rect.left, rect.width, bars.offsetWidth, PLATE_BORDER + PLATE_PADDING, layout.trackLength)\n      const next = clamp(meterUnitToGain(unit, minDecibels, maxDecibels))\n      gain.onChange(next)\n      return next\n    },\n    [gain, clamp, layout.trackLength, minDecibels, maxDecibels],\n  )\n  const onPointerDown = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      if (gain === undefined || event.button !== 0) return\n      event.preventDefault()\n      event.currentTarget.setPointerCapture?.(event.pointerId)\n      ;(event.currentTarget.querySelector(\"[role=slider]\") as HTMLElement | null)?.focus()\n      gain.onChangeStart?.()\n      lastRef.current = fromPointer(event.clientX)\n    },\n    [gain, fromPointer],\n  )\n  const onPointerMove = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      if (!event.currentTarget.hasPointerCapture?.(event.pointerId)) return\n      lastRef.current = fromPointer(event.clientX) ?? lastRef.current\n    },\n    [fromPointer],\n  )\n  const onPointerUp = useCallback(\n    (event: PointerEvent<HTMLDivElement>) => {\n      if (gain === undefined || !event.currentTarget.hasPointerCapture?.(event.pointerId)) return\n      event.currentTarget.releasePointerCapture?.(event.pointerId)\n      gain.onChangeEnd?.(lastRef.current ?? gain.value)\n      lastRef.current = undefined\n    },\n    [gain],\n  )\n  const commit = useCallback(\n    (value: number) => {\n      if (gain === undefined) return\n      const next = clamp(value)\n      gain.onChangeStart?.()\n      gain.onChange(next)\n      gain.onChangeEnd?.(next)\n    },\n    [gain, clamp],\n  )\n  const onKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLSpanElement>) => {\n      if (gain === undefined) return\n      const step = event.shiftKey ? 0.1 : 1\n      let next: number | undefined\n      if (event.key === \"ArrowRight\" || event.key === \"ArrowUp\") next = stepGainDb(gain.value, step, minDecibels)\n      else if (event.key === \"ArrowLeft\" || event.key === \"ArrowDown\") next = stepGainDb(gain.value, -step, minDecibels)\n      else if (event.key === \"Home\") next = gain.min\n      else if (event.key === \"End\") next = gain.max\n      if (next === undefined) return\n      event.preventDefault()\n      commit(next)\n    },\n    [gain, commit, minDecibels],\n  )\n  const onDoubleClick = useCallback(() => {\n    if (gain !== undefined) commit(gain.defaultValue)\n  }, [gain, commit])\n\n  return (\n    <div\n      className={className}\n      data-uikit-output-meter=\"\"\n      data-channels={shown > 1 ? \"stereo\" : \"mono\"}\n      data-adjustable={gain === undefined ? \"false\" : \"true\"}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={onPointerUp}\n      onDoubleClick={gain === undefined ? undefined : onDoubleClick}\n      style={{\n        display: \"inline-flex\",\n        flexDirection: \"column\",\n        alignItems: \"flex-start\",\n        touchAction: \"none\",\n        userSelect: \"none\",\n        cursor: gain === undefined ? undefined : \"ew-resize\",\n        ...style,\n      }}\n    >\n      <div\n        ref={(element) => {\n          barsRef.current = element\n          engine.targets.meter = element\n        }}\n        role=\"meter\"\n        aria-label={ariaLabel}\n        aria-valuemin={minDecibels}\n        aria-valuemax={maxDecibels}\n        aria-valuenow={minDecibels}\n        aria-valuetext={shown > 1 ? \"Left and right channels\" : undefined}\n        style={{\n          boxSizing: \"content-box\",\n          padding: PLATE_PADDING,\n          border: `${PLATE_BORDER}px solid var(--uikit-meter-border, rgba(255, 255, 255, 0.12))`,\n          background: \"var(--uikit-meter-plate, rgba(6, 8, 12, 0.66))\",\n        }}\n      >\n        <MeterBars engine={engine} channels={shown} layout={layout} colors={colors} />\n      </div>\n      {gain === undefined ? null : (\n        <GainHandle\n          gain={gain}\n          unit={gainToMeterUnit(gain.value, minDecibels, maxDecibels)}\n          trackLength={layout.trackLength}\n          onKeyDown={onKeyDown}\n        />\n      )}\n    </div>\n  )\n})\n",
      "type": "registry:component",
      "target": "@ui/output-level-meter.tsx"
    }
  ]
}
