{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-segment-display",
  "type": "registry:ui",
  "title": "Segment Display",
  "description": "The LED readout off the front of a rack unit — 7, 14 or 16 bars per cell, drawn as geometry rather than set in a font, because the thing that makes a real one unmistakable is the bars that are off: showing 1 still shows the ghost of 8. Around that: a recessed pane, a bloom on the lit bars, and light spilling onto the panel in proportion to how much of the display is lit. A plain value with a window of digits, or several captioned fields behind one piece of glass the way a multi-tap delay says what it is doing. Decimal points ride the cell before them, overflow truncates from the end the text is not aligned to, and the cells lean like every module in a rack does.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/segment-display.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/segment-display\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 * SegmentDisplay — the LED readout off the front of a rack unit.\n *\n * Not a font. A segment display is 7, 14 or 16 bars in fixed positions, and the\n * reason a real one is unmistakable is the bars that are **off**: showing `1`\n * on a fourteen-segment cell still shows the ghost of every other bar, dark red\n * on darker glass. A typeface can only draw the lit state, so this draws the\n * geometry — `lib/segment-font` builds the polygons, and the unlit layer is the\n * same list of them in a dimmer colour.\n *\n * Around that sit the other three things a photograph of a panel contains and a\n * styled `<div>` does not, all from `lib/display-chrome`: the pane is recessed\n * (dark overhang along the top, a lip of reflected light along the bottom, a\n * vignette to the corners), the lit bars bloom rather than ending at their own\n * edges, and the light **spills** past the window onto the panel around it in\n * proportion to how much of the display is lit. That last one is why `litRatio`\n * exists in the font core: `88:88` washes the bezel and `1` barely does, which\n * is the difference between a display that is emitting and a shape that is\n * coloured in.\n *\n * ## Two shapes of readout\n *\n * A plain one is a `value` and a window of `digits`:\n *\n *   <SegmentDisplay value={128.5} digits={5} align=\"right\" />\n *\n * A panel one is `fields` — several windows behind one piece of glass, each\n * with its silkscreen caption above it, which is how a multi-tap delay says\n * what it is doing in three words instead of a menu:\n *\n *   <SegmentDisplay fields={[\n *     { label: \"mode\", text: \"DELAY\", digits: 5 },\n *     { label: \"parameter 1\", text: \"4X 60MS\", digits: 7 },\n *     { label: \"parameter 2\", text: \"L+R\", digits: 5 },\n *   ]} />\n *\n * The captions are laid out from the same widths the windows are, so they stay\n * over their own field at any size.\n *\n * `masks` drives the bars directly instead, one mask per cell — the escape\n * hatch from the character generator, and the counterpart of the dot screen's\n * `bitmap`: a bar graph out of the middle bars, a rotating \"busy\" segment, the\n * annunciators a real panel has around its digits.\n *\n * ## Notes\n *\n * A `.` is a bit on the cell before it rather than a cell of its own, so\n * `120.5` is four digits wide, not five. Text longer than its window is\n * truncated from the end it is not aligned to — a right-aligned tempo drops\n * leading digits and keeps the ones that move. `slant` leans the cells about\n * their baseline, because every module in a rack is italic by a few degrees and\n * upright segments are the second-biggest tell after missing ghosts.\n *\n * It is a display and nothing else: no state, no gestures, correct on the first\n * paint, and it renders on the server. A control that wants to be *set* by\n * dragging wraps it — see the tempo readout in `transport-controls`.\n *\n * Colour comes from the skin (`preset`), which is a whole scheme lifted off a\n * real front panel. Three sizes of escape hatch, smallest first: `color` for one\n * display, the `--uikit-display-ink` / `--uikit-display-spill` custom properties\n * (`displayInk` / `displaySpill` in `lib/control-theme`) for every display under\n * a wrapper, or a whole `DisplayPreset` object for a panel nobody has built yet.\n */\n\nimport { useEffect, useId, useMemo, useState } from \"react\"\nimport type { CSSProperties } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  type DisplayPreset,\n  type DisplayPresetId,\n  bezelStyle,\n  bloomRadius,\n  displayPreset,\n  ghostColor,\n  glassSheen,\n  inkColor,\n  paneStyle,\n} from \"maruaudio/ui/shared/display-chrome\"\nimport {\n  type SegmentType,\n  litRatio,\n  maskCells,\n  segmentCells,\n  segmentGeometry,\n} from \"maruaudio/ui/shared/segment-font\"\n\n/** One window of the display, with the caption printed over it. */\nexport interface SegmentField {\n  /** Silkscreen caption above this window — \"mode\", \"parameter 1\". */\n  label?: string\n  text?: string | number\n  /**\n   * Drive the bars directly, one mask per cell, bit 0 being segment A. Ignores\n   * `text` — the escape hatch from the character generator, for the things a\n   * font has no glyph for.\n   */\n  masks?: readonly number[]\n  /** Cells in this window. Defaults to whatever the text needs. */\n  digits?: number\n  /** Which end the text sits at, and which end truncation eats. */\n  align?: \"left\" | \"right\"\n}\n\nexport interface SegmentDisplayProps {\n  /** The readout. Numbers are shown as written — format before passing. */\n  value?: string | number\n  /** Cells in the window. Omit to fit the value. */\n  digits?: number\n  /** Which end the text sits at. Defaults to left, or right for a number. */\n  align?: \"left\" | \"right\"\n\n  /** Several windows behind one pane, each captioned. Overrides `value`. */\n  fields?: readonly SegmentField[]\n\n  /**\n   * Drive the bars directly, one mask per cell, bit 0 being segment A —\n   * `SEGMENT_NAMES[type]` names the bits. Ignores `value`, and is the\n   * counterpart of the dot screen's `bitmap`: a bar graph out of the middle\n   * bars, a rotating \"busy\" segment, the annunciators a real panel has.\n   */\n  masks?: readonly number[]\n\n  /** 7 bars for numbers, 14 for words, 16 for the starburst. */\n  type?: SegmentType\n  /** Panel skin — see `DISPLAY_PRESETS`. A whole preset object also works. */\n  preset?: DisplayPresetId | DisplayPreset\n  /** Override the lit colour for this instance. The ghosts follow it. */\n  color?: string\n\n  /** Cell height in pixels. Everything else is proportional. */\n  size?: number\n  /** Bar thickness as a fraction of the cell height. */\n  weight?: number\n  /** Italic lean in degrees. */\n  slant?: number\n  /** How visible the unlit bars are, 0..1. Defaults to the skin's. */\n  ghost?: number\n  /** Halo around the lit bars, 0..1. Defaults to the skin's. */\n  bloom?: number\n  /** How far light escapes the window, 0..1. Defaults to the skin's. */\n  spill?: number\n  /** Overall brightness of the lit bars, 0..1 — a dimmed panel, or a fade. */\n  brightness?: number\n\n  /** Silkscreen caption above the whole display. */\n  label?: string\n  /** Draw the panel around the pane. Off leaves the bare window. */\n  frame?: boolean\n  /** Fold `.` and `,` into the cell before them. On by default. */\n  points?: boolean\n  /** Blink the lit bars — `true` for 500 ms, or a period in milliseconds. */\n  blink?: boolean | number\n\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\n/** Geometry units the cell is built in. The SVG scales it to `size`. */\nconst CELL = 100\n\nexport function SegmentDisplay({\n  value = \"\",\n  digits,\n  align,\n  fields,\n  masks,\n  type = \"fourteen\",\n  preset: presetProp,\n  color,\n  size = 26,\n  weight = 0.095,\n  slant = 6,\n  ghost,\n  bloom,\n  spill,\n  brightness = 1,\n  label,\n  frame = true,\n  points = true,\n  blink = false,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: SegmentDisplayProps) {\n  const uid = useId().replace(/[^a-zA-Z0-9-]/g, \"\")\n  const preset = displayPreset(presetProp)\n  const lit = inkColor(preset, color)\n  const dark = ghostColor(preset, color, ghost)\n\n  /* ------------------------------------------------------------- blinking */\n  // A display blinks by not being driven, so this gates the lit layer only —\n  // the ghosts stay put, which is what the hardware does and what makes a\n  // blinking readout still read as a readout between flashes.\n  const period = blink === true ? 500 : typeof blink === \"number\" ? blink : 0\n  const [flash, setFlash] = useState(true)\n  useEffect(() => {\n    if (!period) {\n      setFlash(true)\n      return\n    }\n    const timer = setInterval(() => setFlash((on) => !on), period)\n    return () => clearInterval(timer)\n  }, [period])\n\n  /* ------------------------------------------------------------- geometry */\n  const geometry = useMemo(\n    () => segmentGeometry(type, { height: CELL, thickness: CELL * weight, slant }),\n    [type, weight, slant],\n  )\n\n  const windows = useMemo(() => {\n    const list: readonly SegmentField[] =\n      fields ?? [\n        {\n          text: value,\n          masks,\n          digits,\n          align: align ?? (typeof value === \"number\" ? \"right\" : \"left\"),\n        },\n      ]\n    return list.map((field) => {\n      const text = String(field.text ?? \"\")\n      const fieldAlign = field.align ?? align ?? \"left\"\n      const cells = field.masks\n        ? maskCells(field.masks, { digits: field.digits, align: fieldAlign })\n        : segmentCells(text, { type, digits: field.digits, align: fieldAlign, points })\n      return {\n        label: field.label,\n        text,\n        cells,\n        // The last cell's decimal point sits past `advance`, so the window is a\n        // whole number of advances wide plus whatever the lean overhangs.\n        width: cells.length * geometry.advance + geometry.lean,\n      }\n    })\n  }, [fields, value, masks, digits, align, type, points, geometry])\n\n  const glow = useMemo(() => {\n    const all = windows.flatMap((w) => w.cells)\n    return litRatio(all, type) * (flash ? 1 : 0)\n  }, [windows, type, flash])\n\n  const scale = size / CELL\n  const radius = Math.round(size * 0.16)\n  const padX = Math.round(size * 0.42)\n  const padY = Math.round(size * 0.3)\n  const fieldGap = Math.round(size * 0.7)\n  const blur = bloomRadius(preset, CELL * weight, bloom)\n  const captions = windows.some((w) => w.label)\n\n  const spillPreset = spill === undefined ? preset : { ...preset, spillStrength: spill }\n  const readout = windows\n    .map((w) => (w.label ? `${w.label}: ${w.text}` : w.text))\n    .join(\", \")\n\n  const captionStyle: CSSProperties = {\n    fontFamily: token.font,\n    fontSize: Math.max(8, Math.round(size * 0.3)),\n    letterSpacing: \"0.08em\",\n    color: preset.caption,\n    lineHeight: 1.2,\n    whiteSpace: \"nowrap\",\n    overflow: \"hidden\",\n    textOverflow: \"clip\",\n  }\n\n  return (\n    <div\n      role=\"img\"\n      aria-label={ariaLabel ?? (label ? `${label}: ${readout}` : readout)}\n      className={className}\n      style={{\n        display: \"inline-block\",\n        padding: frame ? Math.round(size * 0.34) : 0,\n        ...(frame ? bezelStyle(preset, Math.round(size * 0.26)) : null),\n        ...style,\n      }}\n    >\n      {label && <div style={{ ...captionStyle, marginBottom: Math.round(size * 0.16) }}>{label}</div>}\n\n      {captions && (\n        <div\n          aria-hidden\n          style={{\n            display: \"flex\",\n            gap: fieldGap,\n            padding: `0 ${padX}px`,\n            marginBottom: Math.round(size * 0.14),\n          }}\n        >\n          {windows.map((window, index) => (\n            <span key={index} style={{ ...captionStyle, width: window.width * scale }}>\n              {window.label ?? \"\"}\n            </span>\n          ))}\n        </div>\n      )}\n\n      <div\n        style={{\n          ...paneStyle(spillPreset, { glow, radius, scale: Math.max(0.6, size / 26) }),\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: fieldGap,\n          padding: `${padY}px ${padX}px`,\n        }}\n      >\n        {windows.map((window, index) => (\n          <svg\n            key={index}\n            width={window.width * scale}\n            height={size}\n            viewBox={`0 0 ${window.width} ${CELL}`}\n            style={{ display: \"block\", flex: \"0 0 auto\" }}\n            aria-hidden\n          >\n            {blur > 0 && (\n              <defs>\n                <filter\n                  id={`${uid}-bloom-${index}`}\n                  x=\"-40%\"\n                  y=\"-40%\"\n                  width=\"180%\"\n                  height=\"180%\"\n                  colorInterpolationFilters=\"sRGB\"\n                >\n                  <feGaussianBlur in=\"SourceGraphic\" stdDeviation={blur} result=\"halo\" />\n                  <feMerge>\n                    {/* Twice, so the halo carries past the bar rather than\n                        merely softening its edge. */}\n                    <feMergeNode in=\"halo\" />\n                    <feMergeNode in=\"halo\" />\n                    <feMergeNode in=\"SourceGraphic\" />\n                  </feMerge>\n                </filter>\n              </defs>\n            )}\n\n            {/* Every bar the cell has, driven or not. */}\n            <g fill={dark}>\n              {window.cells.map((_, cellIndex) => (\n                <g key={cellIndex} transform={`translate(${cellIndex * geometry.advance} 0)`}>\n                  {geometry.shapes.map((shape) => (\n                    <polygon key={shape.name} points={shape.points} />\n                  ))}\n                  <polygon points={geometry.point.points} />\n                </g>\n              ))}\n            </g>\n\n            {/* The ones that are on. */}\n            <g\n              fill={lit}\n              opacity={flash ? Math.max(0, Math.min(1, brightness)) : 0}\n              filter={blur > 0 ? `url(#${uid}-bloom-${index})` : undefined}\n            >\n              {window.cells.map((cell, cellIndex) => (\n                <g key={cellIndex} transform={`translate(${cellIndex * geometry.advance} 0)`}>\n                  {geometry.shapes\n                    .filter((shape) => (cell.mask >> shape.bit) & 1)\n                    .map((shape) => (\n                      <polygon key={shape.name} points={shape.points} />\n                    ))}\n                  {cell.dot && <polygon points={geometry.point.points} />}\n                </g>\n              ))}\n            </g>\n          </svg>\n        ))}\n\n        {/* The reflection is in front of the elements, as it is on real glass. */}\n        <div\n          aria-hidden\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: radius,\n            background: glassSheen(preset),\n            pointerEvents: \"none\",\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/segment-display.tsx"
    }
  ]
}
