{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "level-meter",
  "title": "Level Meter",
  "description": "Composable peak and RMS level meter with stereo, hold, scale, readout, and clip indication.",
  "dependencies": [
    "solid-js@^1.9.15",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/use-audio-config",
    "@audiocn-solid/use-frame-source",
    "@audiocn-solid/use-reduced-motion",
    "@audiocn-solid/db-scale",
    "@audiocn-solid/db-readout",
    "@audiocn-solid/clip-indicator",
    "@audiocn-solid/use-visibility"
  ],
  "files": [
    {
      "path": "components/ui/level-meter.tsx",
      "content": "import { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport {\n  For,\n  createContext,\n  createMemo,\n  createSignal,\n  untrack,\n  useContext,\n} from \"solid-js\";\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 { provideContext } from \"@/lib/solid/context\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type { DivDOMProps } from \"@/lib/solid/jsx-types\";\nimport { omitProps } from \"@/lib/solid/props\";\nimport { setRefValue } from \"@/lib/solid/ref\";\nimport type { MutableRef, RefTarget } from \"@/lib/solid/ref\";\nimport { mergeStyleVars } from \"@/lib/solid/style\";\nimport type { StyleValue } from \"@/lib/solid/style\";\nimport { cn } from \"@/lib/utils\";\n\nconst ARIA_INTERVAL_MS = 250;\n\nconst REDUCED_MOTION_INTERVAL_MS = 250;\n\nconst DEFAULT_SEGMENTS = 24;\n\nconst POSITION_EPSILON = 0.0005;\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  channelCount: () => number;\n  frames: FrameSource<MeterFrame>;\n  declared: () => MeterFrame | null;\n  registerChannel: (index: number, element: HTMLElement | null) => void;\n}\n\nconst LevelMeterContext = createContext<LevelMeterContextValue | null>(null);\n\nexport const useLevelMeter = (part: string) => {\n  const context = useContext(LevelMeterContext);\n\n  if (!context) {\n    throw new Error(`${part} must be used inside LevelMeter.`);\n  }\n\n  return context;\n};\n\nconst byFromDb = (zones: MeterZone[]): MeterZone[] => {\n  const sorted: MeterZone[] = [];\n\n  for (const zone of zones) {\n    const index = sorted.findIndex((other) => other.fromDb > zone.fromDb);\n\n    if (index === -1) {\n      sorted.push(zone);\n    } else {\n      sorted.splice(index, 0, zone);\n    }\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\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\n    const last = sorted.at(-1);\n\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\n    return `var(--meter-${zone.zone}) ${start}% ${end}%`;\n  });\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\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\n  if (peakDb === undefined && rmsDb === undefined) {\n    return \"\";\n  }\n\n  return `${peakDb ?? SILENCE_DB}:${rmsDb}`;\n};\n\nconst parseNumber = (text: string | undefined): number | undefined => {\n  const value = Number(text);\n\n  return Number.isNaN(value) ? undefined : value;\n};\n\nconst parseLevels = (key: string): MeterFrame | null => {\n  if (key === \"\") {\n    return null;\n  }\n\n  return {\n    channels: key.split(\"|\").map((entry) => {\n      const [peak, rms] = entry.split(\":\");\n\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  active: boolean | null;\n}\n\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: MutableRef<MeterFrame | null>;\n  reducedMotion: boolean;\n  root: MutableRef<HTMLElement | null>;\n  scale: MeterScale;\n  visible: MutableRef<boolean>;\n}\n\ninterface MeterPainter {\n  paint: (frameMs: number) => boolean;\n  setScale: (scale: MeterScale) => void;\n}\n\nconst silentLevel: ChannelLevel = { peakDb: SILENCE_DB };\n\nconst noop = () => {};\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\n    return next;\n  }\n\n  return previous;\n};\n\ninterface ChannelPaint {\n  db: number;\n  settled: boolean;\n}\n\nconst createMeterPainter = (options: PainterOptions): MeterPainter => {\n  const ballisticsOptions: BallisticsInput = options.reducedMotion\n    ? \"instant\"\n    : options.ballistics;\n\n  const states = new Map<number, ChannelState>();\n  const clock = createPainterClock();\n  let { scale } = options;\n  let clipUntil = 0;\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\n    if (existing && existing.element === element) {\n      return existing;\n    }\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\n    states.set(index, created);\n\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\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\n    if (zone !== state.zone) {\n      state.zone = zone;\n      element.dataset.zone = zone;\n    }\n\n    const active = state.level > 0;\n\n    if (active !== state.active) {\n      state.active = active;\n      element.toggleAttribute(\"data-active\", active);\n    }\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\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\n    if (clipping !== clippingShown) {\n      clippingShown = clipping;\n      root.toggleAttribute(\"data-clipping\", clipping);\n    }\n\n    const text = formatDb(loudest, { floorDb: scale.minDb });\n    const zone = zoneForDb(loudest, scale.zones);\n\n    if (text === ariaShown && zone === rootZoneShown) {\n      return;\n    }\n\n    if (!settled && nowMs - lastAriaMs < ARIA_INTERVAL_MS) {\n      return;\n    }\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    if (!options.visible.current) {\n      return false;\n    }\n\n    const nowMs = clock(frameMs);\n\n    if (\n      options.reducedMotion &&\n      nowMs - lastPaintMs < REDUCED_MOTION_INTERVAL_MS\n    ) {\n      return true;\n    }\n\n    lastPaintMs = nowMs;\n\n    const frame = options.latest.current;\n    let loudest = SILENCE_DB;\n    let settled = true;\n    let inputClipping = false;\n\n    for (const [index, element] of options.channels) {\n      const input = frame?.channels[index] ?? silentLevel;\n\n      if (input.peakDb >= CLIP_THRESHOLD_DB) {\n        clipUntil = nowMs + CLIP_HOLD_MS;\n        inputClipping = true;\n      }\n\n      const painted = paintChannel(index, element, input, nowMs);\n      loudest = Math.max(loudest, painted.db);\n      settled &&= painted.settled;\n    }\n\n    const root = options.root.current;\n\n    if (root) {\n      paintRoot(root, loudest, nowMs, settled);\n    }\n\n    const releasingClip = nowMs < clipUntil && !inputClipping;\n\n    return !settled || releasingClip;\n  };\n\n  return {\n    paint,\n    setScale: (next) => {\n      scale = next;\n    },\n  };\n};\n\ntype DivProps = Omit<\n  DivDOMProps,\n  \"children\" | \"class\" | \"className\" | \"ref\" | \"style\"\n> & {\n  class?: string;\n  className?: string;\n  children?: DivDOMProps[\"children\"];\n  style?: StyleValue;\n  ref?: RefTarget<HTMLDivElement>;\n};\n\nconst DIV_OWN = [\"class\", \"className\", \"children\", \"style\", \"ref\"] as const;\n\nexport const LevelMeterChannels = (props: DivProps) => {\n  const context = useLevelMeter(\"LevelMeterChannels\");\n  const rest = omitProps(props, DIV_OWN);\n\n  return (\n    <div\n      class={cn(\n        \"relative flex min-h-0 min-w-0 flex-1 gap-(--meter-gap)\",\n        context.orientation() === \"horizontal\" ? \"flex-col\" : \"h-full flex-row\",\n        props.class,\n        props.className\n      )}\n      data-orientation={context.orientation()}\n      data-slot=\"level-meter-channels\"\n      ref={(node) => setRefValue(props.ref, node)}\n      style={props.style}\n      {...rest}\n    >\n      {props.children}\n    </div>\n  );\n};\n\nexport interface LevelMeterChannelProps extends DivProps {\n  index?: number;\n}\n\nconst CHANNEL_OWN = [...DIV_OWN, \"index\"] as const;\n\nexport const LevelMeterChannel = (props: LevelMeterChannelProps) => {\n  const context = useLevelMeter(\"LevelMeterChannel\");\n  const rest = omitProps(props, CHANNEL_OWN);\n  const index = () => props.index ?? 0;\n\n  let element: HTMLDivElement | null = null;\n\n  const setRef = (node: HTMLDivElement) => {\n    element = node;\n    context.registerChannel(index(), node);\n    setRefValue(props.ref, node);\n  };\n\n  createCompatEffect(\n    () => index(),\n    (currentIndex) => {\n      if (element) {\n        context.registerChannel(currentIndex, element);\n      }\n\n      return () => context.registerChannel(currentIndex, null);\n    }\n  );\n\n  return (\n    <div\n      class={cn(\n        \"flex min-h-0 min-w-0 [--meter-hold:0] [--meter-level:0] [--meter-rms:0]\",\n        context.orientation() === \"horizontal\" ? \"w-full\" : \"h-full\",\n        props.class,\n        props.className\n      )}\n      data-index={index()}\n      data-orientation={context.orientation()}\n      data-slot=\"level-meter-channel\"\n      ref={setRef}\n      style={props.style}\n      {...rest}\n    >\n      {props.children}\n    </div>\n  );\n};\n\nexport const LevelMeterTrack = (props: DivProps) => {\n  const context = useLevelMeter(\"LevelMeterTrack\");\n  const rest = omitProps(props, DIV_OWN);\n  const horizontal = () => context.orientation() === \"horizontal\";\n\n  return (\n    <div\n      class={cn(\n        \"bg-muted relative overflow-hidden rounded-full\",\n        horizontal()\n          ? \"h-(--meter-thickness) w-full\"\n          : \"h-full w-(--meter-thickness)\",\n        props.class,\n        props.className\n      )}\n      data-orientation={context.orientation()}\n      data-slot=\"level-meter-track\"\n      ref={(node) => setRefValue(props.ref, node)}\n      style={props.style}\n      {...rest}\n    >\n      {context.variant() === \"segmented\" ? (\n        <div\n          aria-hidden={\"true\"}\n          class=\"absolute inset-0 bg-(image:--meter-fill) mask-(--meter-mask) opacity-20\"\n          data-slot=\"level-meter-segments\"\n        />\n      ) : null}\n      {props.children}\n    </div>\n  );\n};\n\nexport interface LevelMeterBarProps extends DivProps {\n  measure?: \"peak\" | \"rms\";\n}\n\nconst BAR_OWN = [...DIV_OWN, \"measure\"] as const;\n\nexport const LevelMeterBar = (props: LevelMeterBarProps) => {\n  const context = useLevelMeter(\"LevelMeterBar\");\n  const rest = omitProps(props, BAR_OWN);\n  const measure = () => props.measure ?? \"peak\";\n  const horizontal = () => context.orientation() === \"horizontal\";\n\n  return (\n    <div\n      aria-hidden={\"true\"}\n      class={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        props.class,\n        props.className\n      )}\n      data-measure={measure()}\n      data-slot=\"level-meter-bar\"\n      ref={(node) => setRefValue(props.ref, node)}\n      style={props.style}\n      {...rest}\n    >\n      <div\n        class={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 = (props: DivProps) => {\n  const context = useLevelMeter(\"LevelMeterHold\");\n  const rest = omitProps(props, DIV_OWN);\n  const horizontal = () => context.orientation() === \"horizontal\";\n\n  return (\n    <div\n      aria-hidden={\"true\"}\n      class={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      ref={(node) => setRefValue(props.ref, node)}\n      style={props.style}\n      {...rest}\n    >\n      <div\n        class={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          props.class,\n          props.className\n        )}\n      />\n    </div>\n  );\n};\n\nexport interface LevelMeterScaleProps extends DbScaleProps {\n  minDb?: never;\n  maxDb?: never;\n  taper?: never;\n  orientation?: never;\n}\n\nexport const LevelMeterScale = (props: LevelMeterScaleProps) => {\n  const context = useLevelMeter(\"LevelMeterScale\");\n  const rest = omitProps(props, [\"class\", \"className\"] as const);\n\n  return (\n    <DbScale\n      class={cn(\n        context.orientation() === \"horizontal\" &&\n          \"absolute top-[calc(100%+var(--meter-gap))] left-0 h-(--meter-scale-size)\",\n        props.class,\n        props.className\n      )}\n      maxDb={context.maxDb()}\n      minDb={context.minDb()}\n      orientation={context.orientation()}\n      taper={context.taper()}\n      {...rest}\n    />\n  );\n};\n\nexport interface LevelMeterValueProps extends DbReadoutProps {\n  source?: never;\n  value?: never;\n}\n\nexport const LevelMeterValue = (props: LevelMeterValueProps) => {\n  const context = useLevelMeter(\"LevelMeterValue\");\n  const rest = omitProps(props, [\"class\", \"className\"] as const);\n\n  const value = createMemo(() => {\n    const declared = context.declared();\n\n    return declared\n      ? readChannel(declared, props.measure ?? \"peak\", props.channel ?? \"max\")\n      : undefined;\n  });\n\n  return (\n    <DbReadout\n      class={props.class ?? props.className ?? \"text-muted-foreground text-xs\"}\n      floorDb={context.minDb()}\n      source={context.declared() ? null : context.frames}\n      value={value()}\n      {...rest}\n    />\n  );\n};\n\nexport interface LevelMeterClipProps extends ClipIndicatorProps {\n  source?: never;\n}\n\nexport const LevelMeterClip = (props: LevelMeterClipProps) => {\n  const context = useLevelMeter(\"LevelMeterClip\");\n\n  return <ClipIndicator source={context.frames} {...props} />;\n};\n\nexport const levelMeterVariants = cva(\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    DivProps,\n    Omit<VariantProps<typeof levelMeterVariants>, \"orientation\"> {\n  source?: FrameSource<MeterFrame> | null;\n  peakDb?: number;\n  rmsDb?: number;\n  channels?: ChannelLevel[];\n  channelCount?: number;\n  minDb?: number;\n  maxDb?: number;\n  zones?: MeterZone[];\n  ballistics?: BallisticsInput;\n  taper?: TaperInput;\n  orientation?: Orientation;\n  variant?: LevelMeterVariant;\n  segments?: number;\n  size?: AudioSize;\n  actionsRef?: RefTarget<LevelMeterActions>;\n}\n\nconst METER_OWN = [\n  ...DIV_OWN,\n  \"source\",\n  \"peakDb\",\n  \"rmsDb\",\n  \"channels\",\n  \"channelCount\",\n  \"minDb\",\n  \"maxDb\",\n  \"zones\",\n  \"ballistics\",\n  \"taper\",\n  \"orientation\",\n  \"variant\",\n  \"segments\",\n  \"size\",\n  \"actionsRef\",\n] as const;\n\nconst styleWithMeterVars = (\n  style: DivProps[\"style\"],\n  fill: string,\n  mask: string\n): StyleValue =>\n  mergeStyleVars(style, {\n    \"--meter-fill\": fill,\n    \"--meter-mask\": mask,\n  });\n\nexport const LevelMeter = (props: LevelMeterProps) => {\n  const config = useAudioConfig();\n  const rest = omitProps(props, METER_OWN);\n\n  const ballistics = () => props.ballistics ?? config.ballistics ?? \"peak\";\n  const dimmed = () => config.dimmed ?? false;\n  const maxDb = () => props.maxDb ?? config.maxDb ?? DEFAULT_MAX_DB;\n  const minDb = () => props.minDb ?? config.minDb ?? DEFAULT_MIN_DB;\n\n  const orientation = () =>\n    props.orientation ?? config.orientation ?? \"horizontal\";\n\n  const size = () => props.size ?? config.size ?? \"default\";\n  const zones = () => props.zones ?? config.zones ?? DEFAULT_ZONES;\n  const variant = () => props.variant ?? \"solid\";\n  const segments = () => props.segments ?? DEFAULT_SEGMENTS;\n  const taperInput = () => props.taper ?? \"linear\";\n\n  const reducedMotion = useReducedMotion();\n  const frames = createFrameEmitter<MeterFrame>();\n  const latestRef: MutableRef<MeterFrame | null> = { current: null };\n  const rootRef: MutableRef<HTMLElement | null> = { current: null };\n  const channelsRef = new Map<number, HTMLElement>();\n  let wake = noop;\n  const [rootElement, setRootElement] = createSignal<HTMLElement | null>(null);\n\n  const visibleRef = useVisibility(rootElement, (visible) => {\n    if (visible) {\n      wake();\n    }\n  });\n\n  const [observedCount, setObservedCount] = createSignal<number | null>(null);\n\n  const accept = (frame: MeterFrame) => {\n    latestRef.current = frame;\n    frames.emit(frame);\n\n    if (visibleRef.current) {\n      wake();\n    }\n  };\n\n  const acceptAndCount = (frame: MeterFrame) => {\n    accept(frame);\n    const count = frame.channels.length;\n\n    if (count > 0) {\n      setObservedCount((previous) => (previous === count ? previous : count));\n    }\n  };\n\n  useFrameSource(() => props.source, acceptAndCount);\n\n  const declarativeKey = createMemo(() =>\n    serializeLevels(props.channels, props.peakDb, props.rmsDb)\n  );\n\n  const declaredCount = createMemo<number | null>(() => {\n    if (props.channels) {\n      return props.channels.length;\n    }\n\n    return declarativeKey() === \"\" ? null : 1;\n  });\n\n  const channelCount = createMemo(\n    () => declaredCount() ?? observedCount() ?? props.channelCount ?? 1\n  );\n\n  const declared = createMemo(() =>\n    props.source ? null : parseLevels(declarativeKey())\n  );\n\n  createCompatEffect(\n    () => parseLevels(declarativeKey()),\n    (frame) => {\n      if (frame) {\n        accept(frame);\n      }\n    }\n  );\n\n  const taperFn = createMemo(() =>\n    resolveTaper(taperInput(), minDb(), maxDb())\n  );\n\n  const registerChannel = (index: number, element: HTMLElement | null) => {\n    if (element) {\n      channelsRef.set(index, element);\n    } else {\n      channelsRef.delete(index);\n    }\n\n    wake();\n  };\n\n  const resolvedBallistics = createMemo(() => resolveBallistics(ballistics()));\n\n  const ballisticsKey = createMemo(() => JSON.stringify(resolvedBallistics()));\n\n  const scale = createMemo<MeterScale>(() => ({\n    maxDb: maxDb(),\n    minDb: minDb(),\n    taper: taperFn(),\n    zones: zones(),\n  }));\n\n  let painter: MeterPainter | null = null;\n\n  createCompatEffect(\n    () => ({\n      ballisticsKey: ballisticsKey(),\n      reducedMotion: reducedMotion(),\n    }),\n    ({ ballisticsKey: _ballisticsKey, reducedMotion: reduce }) => {\n      const nextPainter = createMeterPainter({\n        ballistics: untrack(resolvedBallistics),\n        channels: channelsRef,\n        latest: latestRef,\n        reducedMotion: reduce,\n        root: rootRef,\n        scale: untrack(scale),\n        visible: visibleRef,\n      });\n\n      const task = createFrameTask(nextPainter.paint);\n      painter = nextPainter;\n      wake = task.wake;\n\n      return () => {\n        if (painter === nextPainter) {\n          painter = null;\n        }\n\n        if (wake === task.wake) {\n          wake = noop;\n        }\n\n        task.stop();\n      };\n    }\n  );\n\n  createCompatEffect(scale, (nextScale) => {\n    painter?.setScale(nextScale);\n    wake();\n  });\n\n  const actions: LevelMeterActions = {\n    paint: acceptAndCount,\n    reset: () => accept({ channels: [] }),\n  };\n\n  createCompatEffect(\n    () => props.actionsRef,\n    (ref) => {\n      setRefValue(ref, actions);\n\n      return () => setRefValue(ref, null);\n    }\n  );\n\n  const zoneFill = createMemo(() =>\n    buildZoneFill(zones(), taperFn(), orientation(), variant())\n  );\n\n  const segmentMask = createMemo(() =>\n    variant() === \"segmented\"\n      ? buildSegmentMask(orientation(), segments())\n      : \"none\"\n  );\n\n  const context: LevelMeterContextValue = {\n    channelCount,\n    declared,\n    frames,\n    maxDb,\n    minDb,\n    orientation,\n    registerChannel,\n    taper: taperFn,\n    variant,\n    zones,\n  };\n\n  const setRootRef = (node: HTMLDivElement) => {\n    rootRef.current = node;\n    setRootElement(node);\n    setRefValue(props.ref, node);\n  };\n\n  createCompatEffect(\n    () => rootElement(),\n    () => () => {\n      rootRef.current = null;\n      setRefValue(props.ref, null);\n    }\n  );\n\n  return provideContext(LevelMeterContext, context, () => (\n    <div\n      aria-valuemax={maxDb()}\n      aria-valuemin={minDb()}\n      aria-valuenow={minDb()}\n      class={cn(\n        levelMeterVariants({ orientation: orientation(), size: size() }),\n        props.class,\n        props.className\n      )}\n      data-dimmed={dimmed() ? \"\" : undefined}\n      data-orientation={orientation()}\n      data-size={size()}\n      data-slot=\"level-meter\"\n      data-variant={variant()}\n      ref={setRootRef}\n      role=\"meter\"\n      style={styleWithMeterVars(props.style, zoneFill(), segmentMask())}\n      {...rest}\n    >\n      {props.children ?? (\n        <LevelMeterChannels>\n          <For\n            each={Array.from({ length: channelCount() }, (_, index) => index)}\n          >\n            {(index) => (\n              <LevelMeterChannel index={index}>\n                <LevelMeterTrack>\n                  <LevelMeterBar />\n                  <LevelMeterHold />\n                </LevelMeterTrack>\n              </LevelMeterChannel>\n            )}\n          </For>\n        </LevelMeterChannels>\n      )}\n    </div>\n  ));\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
