{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "level-meter",
  "title": "Level Meter",
  "description": "A peak and RMS level meter: mono or stereo, horizontal or vertical, solid, segmented or gradient, with zones, peak hold, a scale, a readout and a clip light.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn/core",
    "@audiocn/use-audio-config",
    "@audiocn/use-frame-source",
    "@audiocn/use-reduced-motion",
    "@audiocn/db-scale",
    "@audiocn/db-readout",
    "@audiocn/clip-indicator",
    "@audiocn/use-visibility"
  ],
  "files": [
    {
      "path": "components/ui/level-meter.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useEffectEvent,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport type { ComponentProps, CSSProperties, Ref, RefObject } from \"react\";\n\nimport { ClipIndicator } from \"@/components/ui/clip-indicator\";\nimport type { ClipIndicatorProps } from \"@/components/ui/clip-indicator\";\nimport { DbReadout, readChannel } from \"@/components/ui/db-readout\";\nimport type { DbReadoutProps } from \"@/components/ui/db-readout\";\nimport { DbScale } from \"@/components/ui/db-scale\";\nimport type { DbScaleProps } from \"@/components/ui/db-scale\";\nimport { useAudioConfig } from \"@/hooks/use-audio-config\";\nimport type { AudioSize } from \"@/hooks/use-audio-config\";\nimport { useFrameSource } from \"@/hooks/use-frame-source\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibility } from \"@/hooks/use-visibility\";\nimport { createBallistics, resolveBallistics } from \"@/lib/audio/ballistics\";\nimport type { Ballistics, BallisticsInput } from \"@/lib/audio/ballistics\";\nimport {\n  DEFAULT_MAX_DB,\n  DEFAULT_MIN_DB,\n  formatDb,\n  SILENCE_DB,\n} from \"@/lib/audio/decibels\";\nimport { createFrameTask, createPainterClock } from \"@/lib/audio/frame-loop\";\nimport { createFrameEmitter } from \"@/lib/audio/frame-source\";\nimport { resolveTaper } from \"@/lib/audio/taper\";\nimport type { TaperInput } from \"@/lib/audio/taper\";\nimport type {\n  ChannelLevel,\n  FrameSource,\n  MeterFrame,\n  MeterZone,\n  Orientation,\n  Taper,\n} from \"@/lib/audio/types\";\nimport {\n  CLIP_HOLD_MS,\n  CLIP_THRESHOLD_DB,\n  DEFAULT_ZONES,\n  zoneForDb,\n} from \"@/lib/audio/zones\";\nimport { cn } from \"@/lib/utils\";\n\nconst ARIA_INTERVAL_MS = 250;\nconst REDUCED_MOTION_INTERVAL_MS = 250;\nconst DEFAULT_SEGMENTS = 24;\nconst POSITION_EPSILON = 0.0005;\n/**\n * A meter this close to its input has nothing visible left to animate (about\n * 0.06 dB on the default range, under a pixel on any meter), so it sleeps.\n */\nconst SETTLE_EPSILON = 0.001;\n\nexport type LevelMeterVariant = \"solid\" | \"segmented\" | \"gradient\";\n\nexport interface LevelMeterActions {\n  /** Paint a frame directly, for callers that own their own frame loop. */\n  paint: (frame: MeterFrame) => void;\n  /** Drop the level and the peak hold to silence. */\n  reset: () => void;\n}\n\nexport interface LevelMeterContextValue {\n  orientation: Orientation;\n  variant: LevelMeterVariant;\n  minDb: number;\n  maxDb: number;\n  taper: Taper;\n  zones: MeterZone[];\n  /** Tracks the meter shows: declared, seen in the frames, or `channelCount`. */\n  channelCount: number;\n  frames: FrameSource<MeterFrame>;\n  /** The declarative levels, when the meter has values instead of a source. */\n  declared: MeterFrame | null;\n  registerChannel: (index: number, element: HTMLElement | null) => void;\n}\n\nconst LevelMeterContext = createContext<LevelMeterContextValue | null>(null);\n\n/** The surrounding meter's range, scale and frames, for parts built on it. */\nexport const useLevelMeter = (part: string) => {\n  const context = useContext(LevelMeterContext);\n  if (!context) {\n    throw new Error(`${part} must be used inside LevelMeter.`);\n  }\n  return context;\n};\n\n/**\n * Zones in rising order, as a new array. An insertion rather than the ES2023\n * sorted-copy method, which projects compiling against ES2022 lack; a meter\n * has a handful of zones.\n */\nconst byFromDb = (zones: MeterZone[]): MeterZone[] => {\n  const sorted: MeterZone[] = [];\n  for (const zone of zones) {\n    const index = sorted.findIndex((other) => other.fromDb > zone.fromDb);\n    if (index === -1) {\n      sorted.push(zone);\n    } else {\n      sorted.splice(index, 0, zone);\n    }\n  }\n  return sorted;\n};\n\nconst buildZoneFill = (\n  zones: MeterZone[],\n  taper: Taper,\n  orientation: Orientation,\n  variant: LevelMeterVariant\n) => {\n  const direction = orientation === \"horizontal\" ? \"to right\" : \"to top\";\n  const sorted = byFromDb(zones);\n  // Rounded so the server and the browser print the same stops.\n  const starts = sorted.map((zone) =>\n    Number((taper.toPosition(zone.fromDb) * 100).toFixed(3))\n  );\n\n  if (variant === \"gradient\") {\n    const stops = sorted.map(\n      (zone, index) => `var(--meter-${zone.zone}) ${starts[index] ?? 0}%`\n    );\n    const last = sorted.at(-1);\n    return `linear-gradient(${direction}, ${stops.join(\", \")}, var(--meter-${last?.zone ?? \"ok\"}) 100%)`;\n  }\n\n  const stops = sorted.map((zone, index) => {\n    const start = starts[index] ?? 0;\n    const end = starts[index + 1] ?? 100;\n    return `var(--meter-${zone.zone}) ${start}% ${end}%`;\n  });\n  return `linear-gradient(${direction}, ${stops.join(\", \")})`;\n};\n\nconst buildSegmentMask = (orientation: Orientation, segments: number) => {\n  const direction = orientation === \"horizontal\" ? \"to right\" : \"to top\";\n  const size = `calc(100% / ${segments})`;\n  return `repeating-linear-gradient(${direction}, black 0 calc(${size} - 2px), transparent calc(${size} - 2px) ${size})`;\n};\n\nconst serializeLevels = (\n  channels: ChannelLevel[] | undefined,\n  peakDb: number | undefined,\n  rmsDb: number | undefined\n): string => {\n  if (channels) {\n    return channels.map((level) => `${level.peakDb}:${level.rmsDb}`).join(\"|\");\n  }\n  if (peakDb === undefined && rmsDb === undefined) {\n    return \"\";\n  }\n  return `${peakDb ?? SILENCE_DB}:${rmsDb}`;\n};\n\nconst parseNumber = (text: string | undefined): number | undefined => {\n  const value = Number(text);\n  return Number.isNaN(value) ? undefined : value;\n};\n\nconst parseLevels = (key: string): MeterFrame | null => {\n  if (key === \"\") {\n    return null;\n  }\n  return {\n    channels: key.split(\"|\").map((entry) => {\n      const [peak, rms] = entry.split(\":\");\n      return {\n        peakDb: parseNumber(peak) ?? SILENCE_DB,\n        rmsDb: parseNumber(rms),\n      };\n    }),\n  };\n};\n\ninterface ChannelState {\n  element: HTMLElement;\n  peak: Ballistics;\n  rms: Ballistics;\n  level: number;\n  rmsLevel: number;\n  hold: number;\n  zone: string;\n  /** Null until first painted, so a new painter always writes it. */\n  active: boolean | null;\n}\n\n/** Handed to the painter when it changes, so it never rebuilds the painter. */\ninterface MeterScale {\n  maxDb: number;\n  minDb: number;\n  taper: Taper;\n  zones: MeterZone[];\n}\n\ninterface PainterOptions {\n  ballistics: BallisticsInput;\n  channels: Map<number, HTMLElement>;\n  latest: RefObject<MeterFrame | null>;\n  reducedMotion: boolean;\n  root: RefObject<HTMLElement | null>;\n  scale: MeterScale;\n  visible: RefObject<boolean>;\n}\n\ninterface MeterPainter {\n  /** Paints one frame. Returns true while the meter needs another. */\n  paint: (frameMs: number) => boolean;\n  /** A new range, taper or zones, used from the next frame. */\n  setScale: (scale: MeterScale) => void;\n}\n\nconst silentLevel: ChannelLevel = { peakDb: SILENCE_DB };\n\nconst noop = () => {\n  // Nothing to wake before the painter starts.\n};\n\nconst writePosition = (\n  element: HTMLElement,\n  property: string,\n  previous: number,\n  next: number\n) => {\n  if (Math.abs(previous - next) > POSITION_EPSILON) {\n    element.style.setProperty(property, next.toFixed(4));\n    return next;\n  }\n  return previous;\n};\n\ninterface ChannelPaint {\n  /** The smoothed peak this frame, in dBFS. */\n  db: number;\n  /** Level, RMS and hold have all reached the input: nothing left to animate. */\n  settled: boolean;\n}\n\n/**\n * Paints a meter's channels outside React: steps ballistics, writes CSS\n * variables and data attributes, and throttles ARIA updates. Returns true\n * while it needs another frame, so a meter that has settled, or is off\n * screen, stops costing frames until something wakes it.\n */\nconst createMeterPainter = (options: PainterOptions): MeterPainter => {\n  const ballisticsOptions: BallisticsInput = options.reducedMotion\n    ? \"instant\"\n    : options.ballistics;\n  const states = new Map<number, ChannelState>();\n  const clock = createPainterClock();\n  let { scale } = options;\n  let clipUntil = 0;\n  // Null until first painted: a painter rebuilt mid-clip must still clear\n  // the attribute the previous one left.\n  let clippingShown: boolean | null = null;\n  let lastAriaMs = Number.NEGATIVE_INFINITY;\n  let ariaShown: string | null = null;\n  let rootZoneShown: string | null = null;\n  let lastPaintMs = Number.NEGATIVE_INFINITY;\n\n  const stateFor = (index: number, element: HTMLElement) => {\n    const existing = states.get(index);\n    if (existing && existing.element === element) {\n      return existing;\n    }\n    const created: ChannelState = {\n      active: null,\n      element,\n      hold: -1,\n      level: -1,\n      peak: createBallistics(ballisticsOptions),\n      rms: createBallistics({\n        ...resolveBallistics(ballisticsOptions),\n        peakHoldMs: 0,\n      }),\n      rmsLevel: -1,\n      zone: \"\",\n    };\n    states.set(index, created);\n    return created;\n  };\n\n  const paintChannel = (\n    index: number,\n    element: HTMLElement,\n    input: ChannelLevel,\n    nowMs: number\n  ): ChannelPaint => {\n    const state = stateFor(index, element);\n    const inputRmsDb = input.rmsDb ?? input.peakDb;\n    const peak = state.peak.step(input.peakDb, nowMs);\n    const rms = state.rms.step(inputRmsDb, nowMs);\n    const { taper } = scale;\n    const level = taper.toPosition(peak.db);\n    const rmsLevel = taper.toPosition(rms.db);\n    const hold = taper.toPosition(peak.holdDb);\n    state.level = writePosition(element, \"--meter-level\", state.level, level);\n    state.rmsLevel = writePosition(\n      element,\n      \"--meter-rms\",\n      state.rmsLevel,\n      rmsLevel\n    );\n    state.hold = writePosition(element, \"--meter-hold\", state.hold, hold);\n\n    const zone = zoneForDb(peak.db, scale.zones);\n    if (zone !== state.zone) {\n      state.zone = zone;\n      element.dataset.zone = zone;\n    }\n    const active = state.level > 0;\n    if (active !== state.active) {\n      state.active = active;\n      element.toggleAttribute(\"data-active\", active);\n    }\n    const settled =\n      Math.abs(level - taper.toPosition(input.peakDb)) <= SETTLE_EPSILON &&\n      Math.abs(rmsLevel - taper.toPosition(inputRmsDb)) <= SETTLE_EPSILON &&\n      Math.abs(hold - level) <= SETTLE_EPSILON;\n    return { db: peak.db, settled };\n  };\n\n  const paintRoot = (\n    root: HTMLElement,\n    loudest: number,\n    nowMs: number,\n    settled: boolean\n  ) => {\n    const clipping = nowMs < clipUntil;\n    if (clipping !== clippingShown) {\n      clippingShown = clipping;\n      root.toggleAttribute(\"data-clipping\", clipping);\n    }\n    const text = formatDb(loudest, { floorDb: scale.minDb });\n    const zone = zoneForDb(loudest, scale.zones);\n    if (text === ariaShown && zone === rootZoneShown) {\n      return;\n    }\n    // Throttled while the level moves; a settled level is written at once,\n    // so the value a screen reader finds is never a stale one.\n    if (!settled && nowMs - lastAriaMs < ARIA_INTERVAL_MS) {\n      return;\n    }\n    lastAriaMs = nowMs;\n    ariaShown = text;\n    rootZoneShown = zone;\n    const clamped = Math.min(scale.maxDb, Math.max(scale.minDb, loudest));\n    root.setAttribute(\"aria-valuenow\", clamped.toFixed(1));\n    root.setAttribute(\"aria-valuetext\", text);\n    root.dataset.zone = zone;\n  };\n\n  const paint = (frameMs: number): boolean => {\n    // Hidden: sleep. Coming back into view wakes the painter.\n    if (!options.visible.current) {\n      return false;\n    }\n    const nowMs = clock(frameMs);\n    if (\n      options.reducedMotion &&\n      nowMs - lastPaintMs < REDUCED_MOTION_INTERVAL_MS\n    ) {\n      return true;\n    }\n    lastPaintMs = nowMs;\n    const frame = options.latest.current;\n    let loudest = SILENCE_DB;\n    let settled = true;\n    let inputClipping = false;\n    for (const [index, element] of options.channels) {\n      const input = frame?.channels[index] ?? silentLevel;\n      if (input.peakDb >= CLIP_THRESHOLD_DB) {\n        clipUntil = nowMs + CLIP_HOLD_MS;\n        inputClipping = true;\n      }\n      const painted = paintChannel(index, element, input, nowMs);\n      loudest = Math.max(loudest, painted.db);\n      settled &&= painted.settled;\n    }\n    const root = options.root.current;\n    if (root) {\n      paintRoot(root, loudest, nowMs, settled);\n    }\n    // A clip light counting down after the input dropped needs the frames\n    // that turn it off; one held by a still-clipping input does not.\n    const releasingClip = nowMs < clipUntil && !inputClipping;\n    return !settled || releasingClip;\n  };\n\n  return {\n    paint,\n    setScale: (next) => {\n      scale = next;\n    },\n  };\n};\n\nexport const LevelMeterChannels = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation } = useLevelMeter(\"LevelMeterChannels\");\n  return (\n    <div\n      className={cn(\n        \"relative flex min-h-0 min-w-0 flex-1 gap-(--meter-gap)\",\n        orientation === \"horizontal\" ? \"flex-col\" : \"h-full flex-row\",\n        className\n      )}\n      data-orientation={orientation}\n      data-slot=\"level-meter-channels\"\n      {...props}\n    />\n  );\n};\n\nexport interface LevelMeterChannelProps extends ComponentProps<\"div\"> {\n  /** Which channel of the frames this track shows. Default 0. */\n  index?: number;\n}\n\nexport const LevelMeterChannel = ({\n  index = 0,\n  className,\n  ref,\n  ...props\n}: LevelMeterChannelProps) => {\n  const { orientation, registerChannel } = useLevelMeter(\"LevelMeterChannel\");\n\n  const setRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      registerChannel(index, node);\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [index, ref, registerChannel]\n  );\n\n  return (\n    <div\n      className={cn(\n        \"flex min-h-0 min-w-0 [--meter-hold:0] [--meter-level:0] [--meter-rms:0]\",\n        orientation === \"horizontal\" ? \"w-full\" : \"h-full\",\n        className\n      )}\n      data-index={index}\n      data-orientation={orientation}\n      data-slot=\"level-meter-channel\"\n      ref={setRef}\n      {...props}\n    />\n  );\n};\n\nexport const LevelMeterTrack = ({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation, variant } = useLevelMeter(\"LevelMeterTrack\");\n  const horizontal = orientation === \"horizontal\";\n\n  return (\n    <div\n      className={cn(\n        \"bg-muted relative overflow-hidden rounded-full\",\n        horizontal\n          ? \"h-(--meter-thickness) w-full\"\n          : \"h-full w-(--meter-thickness)\",\n        className\n      )}\n      data-orientation={orientation}\n      data-slot=\"level-meter-track\"\n      {...props}\n    >\n      {variant === \"segmented\" ? (\n        <div\n          aria-hidden\n          className=\"absolute inset-0 bg-(image:--meter-fill) mask-(--meter-mask) opacity-20\"\n          data-slot=\"level-meter-segments\"\n        />\n      ) : null}\n      {children}\n    </div>\n  );\n};\n\nexport interface LevelMeterBarProps extends ComponentProps<\"div\"> {\n  /** Which measurement this bar shows. Layer a `rms` and a `peak` bar for a dual meter. Default `peak`. */\n  measure?: \"peak\" | \"rms\";\n}\n\nexport const LevelMeterBar = ({\n  measure = \"peak\",\n  className,\n  ...props\n}: LevelMeterBarProps) => {\n  const { orientation } = useLevelMeter(\"LevelMeterBar\");\n  const horizontal = orientation === \"horizontal\";\n\n  return (\n    <div\n      aria-hidden\n      className={cn(\n        \"absolute inset-0 overflow-hidden\",\n        measure === \"rms\"\n          ? \"[--meter-bar-level:var(--meter-rms)]\"\n          : \"[--meter-bar-level:var(--meter-level)]\",\n        horizontal\n          ? \"translate-x-[calc((var(--meter-bar-level)_-_1)_*_100%)]\"\n          : \"translate-y-[calc((1_-_var(--meter-bar-level))_*_100%)]\",\n        className\n      )}\n      data-measure={measure}\n      data-slot=\"level-meter-bar\"\n      {...props}\n    >\n      <div\n        className={cn(\n          \"absolute inset-0 bg-(image:--meter-fill) mask-(--meter-mask)\",\n          horizontal\n            ? \"translate-x-[calc((1_-_var(--meter-bar-level))_*_100%)]\"\n            : \"translate-y-[calc((var(--meter-bar-level)_-_1)_*_100%)]\"\n        )}\n        data-slot=\"level-meter-fill\"\n      />\n    </div>\n  );\n};\n\nexport const LevelMeterHold = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { orientation } = useLevelMeter(\"LevelMeterHold\");\n  const horizontal = orientation === \"horizontal\";\n\n  return (\n    <div\n      aria-hidden\n      className={cn(\n        \"pointer-events-none absolute inset-0 opacity-[calc(var(--meter-hold)_*_50)]\",\n        horizontal\n          ? \"translate-x-[calc((var(--meter-hold)_-_1)_*_100%)]\"\n          : \"translate-y-[calc((1_-_var(--meter-hold))_*_100%)]\"\n      )}\n      data-slot=\"level-meter-hold\"\n      {...props}\n    >\n      <div\n        className={cn(\n          \"bg-foreground/80 absolute\",\n          horizontal ? \"inset-y-0 right-0 w-0.5\" : \"inset-x-0 top-0 h-0.5\",\n          className\n        )}\n      />\n    </div>\n  );\n};\n\nexport type LevelMeterScaleProps = Omit<\n  DbScaleProps,\n  \"minDb\" | \"maxDb\" | \"taper\" | \"orientation\"\n>;\n\nexport const LevelMeterScale = ({\n  className,\n  ...props\n}: LevelMeterScaleProps) => {\n  const { maxDb, minDb, orientation, taper } = useLevelMeter(\"LevelMeterScale\");\n  // Horizontal meters reserve space below the tracks for the scale.\n  return (\n    <DbScale\n      className={cn(\n        orientation === \"horizontal\" &&\n          \"absolute top-[calc(100%+var(--meter-gap))] left-0 h-(--meter-scale-size)\",\n        className\n      )}\n      maxDb={maxDb}\n      minDb={minDb}\n      orientation={orientation}\n      taper={taper}\n      {...props}\n    />\n  );\n};\n\nexport type LevelMeterValueProps = Omit<DbReadoutProps, \"source\" | \"value\">;\n\nexport const LevelMeterValue = (props: LevelMeterValueProps) => {\n  const { declared, frames, minDb } = useLevelMeter(\"LevelMeterValue\");\n  // Declarative levels only change on render, so show them as a value. A\n  // stream keeps its live readout, which falls to −∞ when it stops.\n  const value = declared\n    ? readChannel(declared, props.measure ?? \"peak\", props.channel ?? \"max\")\n    : undefined;\n  return (\n    <DbReadout\n      className=\"text-muted-foreground text-xs\"\n      floorDb={minDb}\n      source={declared ? null : frames}\n      value={value}\n      {...props}\n    />\n  );\n};\n\nexport type LevelMeterClipProps = Omit<ClipIndicatorProps, \"source\">;\n\nexport const LevelMeterClip = (props: LevelMeterClipProps) => {\n  const { frames } = useLevelMeter(\"LevelMeterClip\");\n  return <ClipIndicator source={frames} {...props} />;\n};\n\nconst levelMeterVariants = cva(\n  // Animated bars and peak markers must not become the page's scroll anchor.\n  \"group/level-meter flex gap-2 [--meter-gap:0.25rem] [overflow-anchor:none] data-dimmed:opacity-50\",\n  {\n    defaultVariants: {\n      orientation: \"horizontal\",\n      size: \"default\",\n    },\n    variants: {\n      orientation: {\n        horizontal:\n          \"w-full flex-row items-center [--meter-scale-size:1rem] has-[[data-slot=db-scale]]:pb-[calc(var(--meter-scale-size)+var(--meter-gap))]\",\n        vertical: \"min-h-32 flex-col items-center\",\n      },\n      size: {\n        default: \"[--meter-thickness:0.5rem]\",\n        lg: \"[--meter-thickness:0.75rem]\",\n        sm: \"[--meter-thickness:0.25rem]\",\n      },\n    },\n  }\n);\n\nexport interface LevelMeterProps\n  extends\n    ComponentProps<\"div\">,\n    Omit<VariantProps<typeof levelMeterVariants>, \"orientation\"> {\n  /** A meter source; the meter subscribes and paints itself without re-rendering React. */\n  source?: FrameSource<MeterFrame> | null;\n  /** Mono peak level in dBFS, for declarative use. */\n  peakDb?: number;\n  /** Mono RMS level in dBFS, for declarative use. */\n  rmsDb?: number;\n  /** Levels per channel, for declarative multi-channel use. */\n  channels?: ChannelLevel[];\n  /** Tracks to render before data arrives. Default 1. */\n  channelCount?: number;\n  /** Bottom of the displayed range. Default −60. */\n  minDb?: number;\n  /** Top of the displayed range. Default 0. */\n  maxDb?: number;\n  /** Colour zones. Default ok / warn from −20 / clip from −9. */\n  zones?: MeterZone[];\n  /** How the meter moves. Default `peak`. */\n  ballistics?: BallisticsInput;\n  /** Scale law. Default `linear`. */\n  taper?: TaperInput;\n  orientation?: Orientation;\n  /** `segmented` is an LED ladder. Default `solid`. */\n  variant?: LevelMeterVariant;\n  /** Segments for the `segmented` variant. Default 24. */\n  segments?: number;\n  actionsRef?: Ref<LevelMeterActions>;\n}\n\ninterface MeterSettings {\n  orientation: Orientation;\n  size: AudioSize;\n  minDb: number;\n  maxDb: number;\n  zones: MeterZone[];\n  ballistics: BallisticsInput;\n  dimmed: boolean;\n}\n\n/** Explicit props win over the surrounding mixer or strip. */\nconst useMeterSettings = (props: Partial<MeterSettings>): MeterSettings => {\n  const config = useAudioConfig();\n  return {\n    ballistics: props.ballistics ?? config.ballistics ?? \"peak\",\n    dimmed: config.dimmed ?? false,\n    maxDb: props.maxDb ?? config.maxDb ?? DEFAULT_MAX_DB,\n    minDb: props.minDb ?? config.minDb ?? DEFAULT_MIN_DB,\n    orientation: props.orientation ?? config.orientation ?? \"horizontal\",\n    size: props.size ?? config.size ?? \"default\",\n    zones: props.zones ?? config.zones ?? DEFAULT_ZONES,\n  };\n};\n\nexport const LevelMeter = ({\n  source,\n  peakDb,\n  rmsDb,\n  channels,\n  channelCount: channelCountProp,\n  minDb: minDbProp,\n  maxDb: maxDbProp,\n  zones: zonesProp,\n  ballistics: ballisticsProp,\n  taper = \"linear\",\n  orientation: orientationProp,\n  variant = \"solid\",\n  segments = DEFAULT_SEGMENTS,\n  size: sizeProp,\n  actionsRef,\n  className,\n  children,\n  ref,\n  style,\n  ...props\n}: LevelMeterProps) => {\n  const settings = useMeterSettings({\n    ballistics: ballisticsProp,\n    maxDb: maxDbProp,\n    minDb: minDbProp,\n    orientation: orientationProp,\n    size: sizeProp ?? undefined,\n    zones: zonesProp,\n  });\n  const { maxDb, minDb, orientation, zones } = settings;\n  const reducedMotion = useReducedMotion();\n  const frames = useMemo(() => createFrameEmitter<MeterFrame>(), []);\n  const latestRef = useRef<MeterFrame | null>(null);\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const channelsRef = useRef(new Map<number, HTMLElement>());\n  // Wakes the painter when something changed. A settled meter sleeps.\n  const wakeRef = useRef<() => void>(noop);\n  const visibleRef = useVisibility(rootRef, (visible) => {\n    if (visible) {\n      wakeRef.current();\n    }\n  });\n  const [observedCount, setObservedCount] = useState<number | null>(null);\n\n  const accept = useCallback(\n    (frame: MeterFrame) => {\n      latestRef.current = frame;\n      frames.emit(frame);\n      wakeRef.current();\n    },\n    [frames]\n  );\n\n  const acceptAndCount = useCallback(\n    (frame: MeterFrame) => {\n      accept(frame);\n      const count = frame.channels.length;\n      if (count > 0) {\n        setObservedCount((previous) => (previous === count ? previous : count));\n      }\n    },\n    [accept]\n  );\n\n  useFrameSource(source, acceptAndCount);\n\n  const declarativeKey = serializeLevels(channels, peakDb, rmsDb);\n  let declaredCount: number | null = null;\n  if (channels) {\n    declaredCount = channels.length;\n  } else if (declarativeKey !== \"\") {\n    declaredCount = 1;\n  }\n  const channelCount = declaredCount ?? observedCount ?? channelCountProp ?? 1;\n  const declared = useMemo(\n    () => (source ? null : parseLevels(declarativeKey)),\n    [declarativeKey, source]\n  );\n\n  useEffect(() => {\n    const frame = parseLevels(declarativeKey);\n    if (frame) {\n      accept(frame);\n    }\n  }, [accept, declarativeKey]);\n\n  const taperFn = useMemo(\n    () => resolveTaper(taper, minDb, maxDb),\n    [taper, minDb, maxDb]\n  );\n\n  const registerChannel = useCallback(\n    (index: number, element: HTMLElement | null) => {\n      if (element) {\n        channelsRef.current.set(index, element);\n      } else {\n        channelsRef.current.delete(index);\n      }\n      wakeRef.current();\n    },\n    []\n  );\n\n  const ballisticsKey = JSON.stringify(\n    typeof settings.ballistics === \"string\"\n      ? settings.ballistics\n      : resolveBallistics(settings.ballistics)\n  );\n\n  // Scale changes (an inline zones array, a new range) reach the painter on\n  // its next frame instead of rebuilding it and resetting the ballistics.\n  const scale = useMemo<MeterScale>(\n    () => ({ maxDb, minDb, taper: taperFn, zones }),\n    [maxDb, minDb, taperFn, zones]\n  );\n  const readScale = useEffectEvent(() => scale);\n  const painterRef = useRef<MeterPainter | null>(null);\n\n  useEffect(() => {\n    const painter = createMeterPainter({\n      ballistics: JSON.parse(ballisticsKey) as BallisticsInput,\n      channels: channelsRef.current,\n      latest: latestRef,\n      reducedMotion,\n      root: rootRef,\n      scale: readScale(),\n      visible: visibleRef,\n    });\n    const task = createFrameTask(painter.paint);\n    painterRef.current = painter;\n    wakeRef.current = task.wake;\n    return () => {\n      painterRef.current = null;\n      wakeRef.current = noop;\n      task.stop();\n    };\n  }, [ballisticsKey, reducedMotion, visibleRef]);\n\n  // A new range or zones array reaches the painter, and repaints a meter that\n  // had settled.\n  useEffect(() => {\n    painterRef.current?.setScale(scale);\n    wakeRef.current();\n  }, [scale]);\n\n  useImperativeHandle(\n    actionsRef,\n    () => ({\n      paint: acceptAndCount,\n      reset: () => {\n        accept({ channels: [] });\n      },\n    }),\n    [accept, acceptAndCount]\n  );\n\n  const zoneFill = useMemo(\n    () => buildZoneFill(zones, taperFn, orientation, variant),\n    [orientation, taperFn, variant, zones]\n  );\n  const segmentMask =\n    variant === \"segmented\" ? buildSegmentMask(orientation, segments) : \"none\";\n\n  const contextValue = useMemo<LevelMeterContextValue>(\n    () => ({\n      channelCount,\n      declared,\n      frames,\n      maxDb,\n      minDb,\n      orientation,\n      registerChannel,\n      taper: taperFn,\n      variant,\n      zones,\n    }),\n    [\n      channelCount,\n      declared,\n      frames,\n      maxDb,\n      minDb,\n      orientation,\n      registerChannel,\n      taperFn,\n      variant,\n      zones,\n    ]\n  );\n\n  const setRootRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [ref]\n  );\n\n  return (\n    <LevelMeterContext.Provider value={contextValue}>\n      <div\n        aria-valuemax={maxDb}\n        aria-valuemin={minDb}\n        aria-valuenow={minDb}\n        className={cn(\n          levelMeterVariants({ orientation, size: settings.size }),\n          className\n        )}\n        data-dimmed={settings.dimmed ? \"\" : undefined}\n        data-orientation={orientation}\n        data-size={settings.size}\n        data-slot=\"level-meter\"\n        data-variant={variant}\n        ref={setRootRef}\n        role=\"meter\"\n        style={\n          {\n            \"--meter-fill\": zoneFill,\n            \"--meter-mask\": segmentMask,\n            ...style,\n          } as CSSProperties\n        }\n        {...props}\n      >\n        {children ?? (\n          <LevelMeterChannels>\n            {Array.from({ length: channelCount }, (_, index) => (\n              <LevelMeterChannel index={index} key={`channel-${index}`}>\n                <LevelMeterTrack>\n                  <LevelMeterBar />\n                  <LevelMeterHold />\n                </LevelMeterTrack>\n              </LevelMeterChannel>\n            ))}\n          </LevelMeterChannels>\n        )}\n      </div>\n    </LevelMeterContext.Provider>\n  );\n};\n\nexport { levelMeterVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}