{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lcd-meter",
  "title": "LCD Meter",
  "description": "A recorder-style level meter on an LCD Display's screen: block segments in the display's ink that light whole, a held peak segment, per-channel dB readouts, a printed scale with a bracket over the clip zone and a CLIP flag, in light and dark.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn/core",
    "@audiocn/db-scale",
    "@audiocn/lcd-display",
    "@audiocn/level-meter"
  ],
  "files": [
    {
      "path": "components/ui/lcd-meter.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useLayoutEffect,\n  useRef,\n} from \"react\";\nimport type { ComponentProps, CSSProperties, RefObject } from \"react\";\n\nimport { DbScale } from \"@/components/ui/db-scale\";\nimport { useLcdDisplay } from \"@/components/ui/lcd-display\";\nimport {\n  LevelMeter,\n  LevelMeterChannel,\n  LevelMeterClip,\n  LevelMeterValue,\n  useLevelMeter,\n} from \"@/components/ui/level-meter\";\nimport type {\n  LevelMeterChannelProps,\n  LevelMeterClipProps,\n  LevelMeterProps,\n  LevelMeterValueProps,\n} from \"@/components/ui/level-meter\";\nimport { formatDb } from \"@/lib/audio/decibels\";\nimport { cn } from \"@/lib/utils\";\n\n/** The marks a recorder screen prints, kept to the meter's range. */\nconst DEFAULT_TICKS = [6, 3, 0, -3, -6, -12, -24, -48];\n/** Off the dot grid by less than this, in pixels, counts as on it. */\nconst GRID_EPSILON = 0.01;\n\n/**\n * Segments in dots of the display's pitch, so every segment edge falls\n * between two dots. Each thickness centres the track on a line of text of\n * that size with a whole number of dots above and below it.\n */\nconst lcdMeterVariants = cva(\n  \"[--lcd-meter-gap:var(--lcd-display-pitch)] [--lcd-meter-nudge-x:0px] [--lcd-meter-nudge-y:0px] [--lcd-meter-segment:calc(var(--lcd-display-pitch)*3)] [--lcd-meter-unlit:color-mix(in_oklab,var(--lcd-display-lit)_14%,transparent)]\",\n  {\n    defaultVariants: {\n      size: \"default\",\n    },\n    variants: {\n      size: {\n        default: \"[--lcd-meter-thickness:calc(var(--lcd-display-pitch)*7)]\",\n        lg: \"[--lcd-meter-thickness:calc(var(--lcd-display-pitch)*10)]\",\n        sm: \"[--lcd-meter-thickness:calc(var(--lcd-display-pitch)*5)]\",\n      },\n    },\n  }\n);\n\n/** A segment and the gap after it. */\nconst PERIOD =\n  \"[--lcd-meter-period:calc(var(--lcd-meter-segment)_+_var(--lcd-meter-gap))]\";\n\n/**\n * Starts on the dot grid, and cuts the column down to whole segments so the\n * last one is never cut.\n */\nconst ON_GRID =\n  \"ml-(--lcd-meter-nudge-x) w-[calc(round(down,_100%_-_var(--lcd-meter-nudge-x)_+_var(--lcd-meter-gap),_var(--lcd-meter-period))_-_var(--lcd-meter-gap))]\";\n\nconst SEGMENT_MASK =\n  \"mask-[repeating-linear-gradient(to_right,#000_0_var(--lcd-meter-segment),transparent_var(--lcd-meter-segment)_var(--lcd-meter-period))]\";\n\n/** The display's dot grid, for a track on a screen whose own mask is off. */\nconst DOT_MASK =\n  \"mask-[radial-gradient(circle,#000_58%,transparent_64%)] mask-size-[var(--lcd-display-pitch)_var(--lcd-display-pitch)]\";\n\n/** The channel a readout inside a row shows. */\nconst ChannelContext = createContext<number | null>(null);\n\n/** L and R for a pair, numbers for more, nothing for one. */\nconst channelLabel = (index: number, count: number) => {\n  if (count === 2) {\n    return index === 0 ? \"L\" : \"R\";\n  }\n  return count > 2 ? String(index + 1) : null;\n};\n\n/** How far short of the next dot a distance ends, or 0 on a dot. */\nconst toNextDot = (distance: number, pitch: number) => {\n  const past = ((distance % pitch) + pitch) % pitch;\n  return past < GRID_EPSILON || pitch - past < GRID_EPSILON ? 0 : pitch - past;\n};\n\n/**\n * The screen's dot pitch in pixels, read from its dot mask, which the browser\n * reports in pixels. 0 with the mask off, when there is no grid to meet.\n */\nconst pitchOf = (content: HTMLElement) => {\n  const [width = \"\"] = (getComputedStyle(content).maskSize || \"\").split(\" \");\n  return width.endsWith(\"px\") ? Number(width.slice(0, -\"px\".length)) : 0;\n};\n\n/**\n * The screen's dot mask starts at its content box, so a segment edge falls\n * between dots only when the tracks start a whole number of dots from it.\n * Labels of any width, or rows above of any height, can leave them part of a\n * dot off; this measures the first track and moves the tracks onto the grid.\n */\nconst useDotGrid = (rootRef: RefObject<HTMLDivElement | null>) => {\n  const nudgeRef = useRef({ x: 0, y: 0 });\n  // Every render: a new label or row changes the layout without a resize.\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    const content = root?.closest<HTMLElement>(\n      \"[data-slot=lcd-display-content]\"\n    );\n    if (!(root && content)) {\n      return;\n    }\n    const align = () => {\n      const nudge = nudgeRef.current;\n      const track = root.querySelector<HTMLElement>(\n        \"[data-slot=lcd-meter-track]\"\n      );\n      const pitch = pitchOf(content);\n      let x = 0;\n      let y = 0;\n      if (track && pitch > 0) {\n        const origin = content.getBoundingClientRect();\n        const rect = track.getBoundingClientRect();\n        x = toNextDot(rect.left - nudge.x - origin.left, pitch);\n        y = toNextDot(rect.top - nudge.y - origin.top, pitch);\n      }\n      if (x !== nudge.x) {\n        nudge.x = x;\n        root.style.setProperty(\"--lcd-meter-nudge-x\", `${x}px`);\n      }\n      if (y !== nudge.y) {\n        nudge.y = y;\n        root.style.setProperty(\"--lcd-meter-nudge-y\", `${y}px`);\n      }\n    };\n    align();\n    if (typeof ResizeObserver === \"undefined\") {\n      return;\n    }\n    const resize = new ResizeObserver(align);\n    resize.observe(content);\n    for (const label of root.querySelectorAll(\"[data-slot=lcd-meter-label]\")) {\n      resize.observe(label);\n    }\n    return () => {\n      resize.disconnect();\n    };\n  });\n};\n\n/** Magnitudes, as recorder screens print them; above 0 keeps its plus. */\nconst printMagnitude = (db: number) =>\n  formatDb(db, { decimals: 0, sign: db > 0 ? \"auto\" : \"never\", unit: false });\n\nexport type LcdMeterChannelProps = LevelMeterChannelProps;\n\n/** One row of the meter: a label, a track and a readout for one channel. */\nexport const LcdMeterChannel = ({\n  index = 0,\n  className,\n  ...props\n}: LcdMeterChannelProps) => (\n  <ChannelContext.Provider value={index}>\n    <LevelMeterChannel\n      className={cn(\n        \"col-span-full grid grid-cols-subgrid items-center\",\n        className\n      )}\n      data-slot=\"lcd-meter-channel\"\n      index={index}\n      {...props}\n    />\n  </ChannelContext.Provider>\n);\n\n/** Dim text naming a channel, in a column a whole number of dots wide. */\nexport const LcdMeterLabel = ({\n  className,\n  ...props\n}: ComponentProps<\"span\">) => (\n  <span\n    className={cn(\n      \"col-start-1 min-w-[round(up,_1.5em,_var(--lcd-display-pitch))] text-(--lcd-display-ink-dim)\",\n      className\n    )}\n    data-slot=\"lcd-meter-label\"\n    {...props}\n  />\n);\n\n/** The unlit segments, which the bar and the held peak light. */\nexport const LcdMeterTrack = ({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const matrix = useLcdDisplay()?.matrix ?? true;\n  return (\n    <div\n      className={cn(\n        \"relative col-start-2 h-(--lcd-meter-thickness)\",\n        PERIOD,\n        ON_GRID,\n        !matrix && DOT_MASK,\n        className\n      )}\n      data-slot=\"lcd-meter-track\"\n      {...props}\n    >\n      <div\n        className={cn(\"absolute inset-0 bg-(--lcd-meter-unlit)\", SEGMENT_MASK)}\n        data-slot=\"lcd-meter-segments\"\n      >\n        {children}\n      </div>\n    </div>\n  );\n};\n\nexport interface LcdMeterBarProps 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\n/** The lit segments. A segment lights whole or not at all. */\nexport const LcdMeterBar = ({\n  measure = \"peak\",\n  className,\n  ...props\n}: LcdMeterBarProps) => (\n  <div\n    aria-hidden\n    className={cn(\n      \"absolute inset-0 bg-(--lcd-display-lit) [clip-path:inset(0_calc(100%_-_round(up,_var(--lcd-meter-bar-level)_*_100%,_var(--lcd-meter-period)))_0_0)]\",\n      measure === \"rms\"\n        ? \"[--lcd-meter-bar-level:var(--meter-rms)]\"\n        : \"[--lcd-meter-bar-level:var(--meter-level)]\",\n      className\n    )}\n    data-measure={measure}\n    data-slot=\"lcd-meter-bar\"\n    {...props}\n  />\n);\n\n/** One lit segment at the held peak, hidden at silence. */\nexport const LcdMeterHold = ({\n  className,\n  ...props\n}: ComponentProps<\"div\">) => (\n  <div\n    aria-hidden\n    className={cn(\n      \"absolute inset-y-0 left-[calc(round(up,_var(--meter-hold)_*_100%,_var(--lcd-meter-period))_-_var(--lcd-meter-period))] w-(--lcd-meter-segment) bg-(--lcd-display-lit) opacity-[calc(var(--meter-hold)_*_1000)]\",\n      className\n    )}\n    data-slot=\"lcd-meter-hold\"\n    {...props}\n  />\n);\n\nexport type LcdMeterValueProps = LevelMeterValueProps;\n\n/**\n * The level in bright digits, without a unit. Inside a channel it shows that\n * channel, elsewhere the loudest one.\n */\nexport const LcdMeterValue = ({\n  channel,\n  unit = false,\n  className,\n  ...props\n}: LcdMeterValueProps) => {\n  const index = useContext(ChannelContext);\n  return (\n    <LevelMeterValue\n      channel={channel ?? index ?? \"max\"}\n      // The readout sizes itself in ch; 1.2 adds the screen's 0.12em of\n      // tracking per character of a 0.6em mono digit, so it never changes width.\n      className={cn(\n        \"col-start-3 min-w-[calc(var(--db-readout-width)*1.2)] font-(family-name:--lcd-display-font) text-(--lcd-display-lit)\",\n        className\n      )}\n      data-slot=\"lcd-meter-value\"\n      unit={unit}\n      {...props}\n    />\n  );\n};\n\nexport interface LcdMeterScaleProps extends ComponentProps<\"div\"> {\n  /** Tick values. Default: 48, 24, 12, 6, 3 and 0 below, inside the range. */\n  ticks?: number[];\n  /** Text for each tick. Default: the magnitude, with a plus above 0. */\n  format?: (db: number) => string;\n  /** Print a bracket over the clip zone. Default true. */\n  bracket?: boolean;\n}\n\n/** Dim numbers under the tracks, and a bracket over the clip zone. */\nexport const LcdMeterScale = ({\n  ticks,\n  format = printMagnitude,\n  bracket = true,\n  className,\n  ...props\n}: LcdMeterScaleProps) => {\n  const { maxDb, minDb, taper, zones } = useLevelMeter(\"LcdMeterScale\");\n  let clipFromDb = Number.POSITIVE_INFINITY;\n  for (const zone of zones) {\n    if (zone.zone === \"clip\") {\n      clipFromDb = Math.min(clipFromDb, zone.fromDb);\n    }\n  }\n  const showBracket = bracket && clipFromDb < maxDb;\n  return (\n    <div\n      className={cn(\n        \"col-start-2 flex flex-col gap-(--lcd-display-pitch)\",\n        PERIOD,\n        ON_GRID,\n        className\n      )}\n      data-slot=\"lcd-meter-scale\"\n      {...props}\n    >\n      {showBracket ? (\n        <div\n          aria-hidden\n          className=\"relative h-[calc(var(--lcd-display-pitch)*2)]\"\n          data-slot=\"lcd-meter-bracket\"\n          style={\n            {\n              \"--lcd-meter-bracket-start\": taper\n                .toPosition(clipFromDb)\n                .toFixed(4),\n            } as CSSProperties\n          }\n        >\n          {/* Starts where the first segment of the zone does. */}\n          <div className=\"absolute inset-y-0 right-0 left-[round(down,_var(--lcd-meter-bracket-start)_*_100%,_var(--lcd-meter-period))] border-x-(length:--lcd-display-pitch) border-t-(length:--lcd-display-pitch) border-(--lcd-display-ink-dim)\" />\n        </div>\n      ) : null}\n      <DbScale\n        className=\"h-[round(up,_1em,_var(--lcd-display-pitch))] text-[0.75em] text-(--lcd-display-ink-dim) [&_[data-slot=db-scale-mark]]:hidden\"\n        format={format}\n        maxDb={maxDb}\n        minDb={minDb}\n        orientation=\"horizontal\"\n        taper={taper}\n        ticks={\n          ticks ??\n          DEFAULT_TICKS.filter((tick) => tick >= minDb && tick <= maxDb)\n        }\n      />\n    </div>\n  );\n};\n\nexport type LcdMeterClipProps = LevelMeterClipProps;\n\n/** A CLIP flag: an unlit ghost that inverts while the clip hold is on. */\nexport const LcdMeterClip = ({\n  className,\n  children,\n  ...props\n}: LcdMeterClipProps) => (\n  <LevelMeterClip\n    className={cn(\n      \"col-start-3 h-auto self-end justify-self-end rounded-sm px-1 py-0.5 text-[0.75em] font-normal text-(--lcd-meter-unlit) uppercase transition-none hover:bg-transparent focus-visible:ring-1 focus-visible:ring-(--lcd-display-lit) data-clipping:bg-(--lcd-display-lit) data-clipping:text-(--lcd-display-glass)\",\n      className\n    )}\n    data-slot=\"lcd-meter-clip\"\n    {...props}\n  >\n    {children ?? \"Clip\"}\n  </LevelMeterClip>\n);\n\nconst LcdMeterRows = () => {\n  const { channelCount } = useLevelMeter(\"LcdMeter\");\n  return (\n    <>\n      {Array.from({ length: channelCount }, (_, index) => {\n        const label = channelLabel(index, channelCount);\n        return (\n          <LcdMeterChannel index={index} key={`channel-${index}`}>\n            {label ? <LcdMeterLabel>{label}</LcdMeterLabel> : null}\n            <LcdMeterTrack>\n              <LcdMeterBar />\n              <LcdMeterHold />\n            </LcdMeterTrack>\n            <LcdMeterValue />\n          </LcdMeterChannel>\n        );\n      })}\n      <LcdMeterScale />\n      <LcdMeterClip />\n    </>\n  );\n};\n\nexport type LcdMeterProps = Omit<\n  LevelMeterProps,\n  \"orientation\" | \"segments\" | \"size\" | \"variant\"\n>;\n\n/**\n * A level meter drawn on an `LcdDisplay`'s screen: block segments in the\n * display's ink, a held peak, readouts, a scale and a CLIP flag. Metering is\n * `LevelMeter`'s; the size and colours come from the display. The `audio`\n * taper gives the loud end of the range most of the track, like a recorder.\n */\nexport const LcdMeter = ({\n  taper = \"audio\",\n  className,\n  children,\n  ref,\n  ...props\n}: LcdMeterProps) => {\n  const display = useLcdDisplay();\n  const rootRef = useRef<HTMLDivElement | null>(null);\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  useDotGrid(rootRef);\n  if (!display) {\n    throw new Error(\"LcdMeter must be used inside LcdDisplay.\");\n  }\n  return (\n    <LevelMeter\n      className={cn(\n        lcdMeterVariants({ size: display.size }),\n        \"grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-[calc(var(--lcd-display-pitch)*4)] gap-y-[calc(var(--lcd-display-pitch)*2)] pt-[calc(0.75rem_+_var(--lcd-meter-nudge-y))] pb-3 has-[[data-slot=db-scale]]:pb-3\",\n        className\n      )}\n      data-slot=\"lcd-meter\"\n      orientation=\"horizontal\"\n      ref={setRootRef}\n      size={display.size}\n      taper={taper}\n      {...props}\n    >\n      {children ?? <LcdMeterRows />}\n    </LevelMeter>\n  );\n};\n\nexport { lcdMeterVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}