{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-status-chip",
  "type": "registry:ui",
  "title": "Status Chip",
  "description": "The LED-style chip a face uses to say what a facility is doing — AUDITION lit, HOST SYNC off, LIVE — in five states that stay tellable apart. The distinction it exists for is off (present, not engaged: label fully readable, lamp dark) versus dim (not available in this host at all: the whole chip recedes), because drawing \"no telemetry from this host\" the same as \"telemetry switched off\" tells the user something false about their session. warn is the second accent — on, but not the state you asked for — and live is the only one that pulses. Presentational and stateless: the host decides which state is true. data-status-chip and data-state let a face's render test assert on the state without matching on copy.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/status-chip.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/status-chip\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 * StatusChip — a small LED-style chip that says what a facility is doing, in\n * five states that are deliberately distinguishable at a glance.\n *\n * Faces keep reinventing this: \"AUDITION lit\", \"HOST SYNC on / off\",\n * \"LIVE / standby\", \"no tap from this host\". Every one of them is a label, a\n * lamp, and a state — and the states that matter are not a boolean. The\n * important distinction is between OFF (a facility that exists and is not\n * engaged) and DIM (a facility that is not available here at all): a face that\n * draws \"no telemetry from this host\" the same as \"telemetry off\" has told the\n * user something false about their session. `warn` is the middle ground — on,\n * but not the state you asked for — and `live` is the only one that animates.\n *\n * The chip is presentational. It holds no state, resolves no capability and\n * makes no claim of its own; the host decides which state is true and the chip\n * draws it. Wrap it in a button if it should be pressable — `StatusChipGroup`\n * only lays chips out on one line.\n *\n * Colours resolve through `token()`, so a themed host repaints the whole strip\n * without this component knowing; `color` overrides the lamp for one chip.\n * `data-status-chip` and `data-state` are on the root so a face's render test\n * can assert \"the host-sync chip says off\" without matching on copy.\n *\n * SSR-safe: inline styles only, no DOM access, nothing measured.\n */\n\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { UI_KIT_PORTABLE_STYLES } from \"maruaudio/ui/shared/portable-styles\"\nimport { token } from \"maruaudio/ui/shared/theme\"\n\n/**\n * What the lamp is doing.\n *\n * - `off`   — the facility is here and not engaged. Lamp dark, label readable.\n * - `dim`   — the facility is unavailable in this host/session. Everything\n *             muted, so \"cannot\" never reads as \"is not\".\n * - `lit`   — engaged and doing its job.\n * - `warn`  — engaged, but not in the state that was asked for (a fallback\n *             source, a degraded lane, a value past its budget).\n * - `live`  — engaged and actively receiving. The only state that pulses.\n */\nexport type StatusChipState = \"off\" | \"dim\" | \"lit\" | \"warn\" | \"live\"\n\nexport interface StatusChipProps {\n  /** The facility's name. Short — this is a chip, not a sentence. */\n  label: ReactNode\n  /** Which state to draw. */\n  state: StatusChipState\n  /** A value or qualifier shown after the label, in muted type. */\n  detail?: ReactNode\n  /** Override the lamp colour for this chip. Defaults to the state's token. */\n  color?: string\n  /** Compact type and padding, for a crowded bottom rail. Defaults to false. */\n  compact?: boolean\n  className?: string\n  style?: CSSProperties\n}\n\n/**\n * How one state is drawn, as plain data.\n *\n * The whole visual contract of the chip is these five rows, so it is a pure\n * function rather than a `switch` buried in JSX — the properties that matter\n * (\"off and dim are never identical\", \"only lit states glow\", \"only live\n * pulses\") are then asserted in Node instead of eyeballed on a demo page.\n */\nexport interface StatusChipAppearance {\n  /** Which theme token the lamp reads, before any `color` override. */\n  readonly lamp: \"accent\" | \"accent2\" | \"muted\"\n  /** Opacity of the whole chip. Only `dim` mutes its own label. */\n  readonly opacity: number\n  /** Opacity of the lamp itself. */\n  readonly lampOpacity: number\n  /** Lit states get a halo and a coloured border; unlit ones get neither. */\n  readonly glow: boolean\n  /** Only `live` animates — a pulse means \"arriving now\", nothing else. */\n  readonly pulse: boolean\n}\n\nexport function statusChipAppearance(\n  state: StatusChipState\n): StatusChipAppearance {\n  switch (state) {\n    case \"live\":\n      return { lamp: \"accent\", opacity: 1, lampOpacity: 1, glow: true, pulse: true }\n    case \"lit\":\n      return { lamp: \"accent\", opacity: 1, lampOpacity: 1, glow: true, pulse: false }\n    case \"warn\":\n      return { lamp: \"accent2\", opacity: 1, lampOpacity: 1, glow: true, pulse: false }\n    case \"off\":\n      // Present but not engaged: the label stays fully readable, the lamp goes\n      // dark. This is what must never look like \"unavailable\".\n      return { lamp: \"muted\", opacity: 1, lampOpacity: 0.35, glow: false, pulse: false }\n    case \"dim\":\n    default:\n      // Not available in this host at all: the whole chip recedes, so \"cannot\"\n      // reads differently from \"is not\".\n      return { lamp: \"muted\", opacity: 0.45, lampOpacity: 0.5, glow: false, pulse: false }\n  }\n}\n\n/** Resolve the appearance's lamp slot to an actual token value. */\nfunction lampToken(lamp: StatusChipAppearance[\"lamp\"]): string {\n  return lamp === \"accent\"\n    ? token.accent\n    : lamp === \"accent2\"\n      ? token.accent2\n      : token.textMuted\n}\n\nexport function StatusChip({\n  label,\n  state,\n  detail,\n  color,\n  compact = false,\n  className,\n  style,\n}: StatusChipProps) {\n  const look = statusChipAppearance(state)\n  const lamp = color ?? lampToken(look.lamp)\n  const lit = look.glow\n  const size = compact ? 5 : 6\n\n  return (\n    <span\n      data-status-chip=\"\"\n      data-state={state}\n      className={className}\n      style={{\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: compact ? 5 : 6,\n        padding: compact ? \"2px 6px\" : \"3px 8px\",\n        border: `1px solid ${lit ? lamp : token.border}`,\n        borderRadius: token.radius,\n        background: token.surface,\n        color: token.text,\n        fontFamily: token.font,\n        fontSize: compact ? 9 : 10,\n        fontWeight: 600,\n        letterSpacing: \"0.12em\",\n        lineHeight: 1,\n        textTransform: \"uppercase\",\n        whiteSpace: \"nowrap\",\n        opacity: look.opacity,\n        ...style,\n      }}\n    >\n      {/*\n        The pulse is a keyframe, which an inline style cannot express, so it\n        lives in `portable-styles` — and is carried here rather than assumed of\n        the host, the same way waveform-selector and module-navbar do it. A\n        registry consumer who copies this file gets the animation; a chip that\n        is not `live` needs no keyframe and injects nothing.\n      */}\n      {look.pulse ? (\n        <style data-maru-ui-kit-portable>{UI_KIT_PORTABLE_STYLES}</style>\n      ) : null}\n      <span\n        aria-hidden\n        data-status-chip-lamp=\"\"\n        style={{\n          width: size,\n          height: size,\n          flexShrink: 0,\n          borderRadius: \"50%\",\n          background: lamp,\n          opacity: look.lampOpacity,\n          // The lit states get a halo; off/dim deliberately get none, so the\n          // difference is visible without relying on colour alone. The pulse is\n          // CSS from `portable-styles`, keyed off `data-state=\"live\"`, because\n          // an inline style cannot express a keyframe.\n          boxShadow: lit ? `0 0 6px ${lamp}` : \"none\",\n        }}\n      />\n      <span data-status-chip-label=\"\">{label}</span>\n      {detail !== undefined && detail !== null ? (\n        <span\n          data-status-chip-detail=\"\"\n          style={{\n            color: token.textMuted,\n            fontFamily: token.fontMono,\n            fontWeight: 500,\n            letterSpacing: \"0.04em\",\n            textTransform: \"none\",\n          }}\n        >\n          {detail}\n        </span>\n      ) : null}\n    </span>\n  )\n}\n\nexport interface StatusChipGroupProps {\n  children: ReactNode\n  /** Gap between chips, in px. Defaults to 6. */\n  gap?: number\n  /** Let chips wrap onto a second line when the rail is narrow. Default true. */\n  wrap?: boolean\n  className?: string\n  style?: CSSProperties\n}\n\n/**\n * A row of chips on one baseline.\n *\n * Deliberately trivial — flex, a gap and a wrap flag. It exists so a face's\n * status rail is one element with one `data-status-chip-group` hook rather than\n * a bare div that every face styles slightly differently.\n */\nexport function StatusChipGroup({\n  children,\n  gap = 6,\n  wrap = true,\n  className,\n  style,\n}: StatusChipGroupProps) {\n  return (\n    <div\n      data-status-chip-group=\"\"\n      className={className}\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap,\n        flexWrap: wrap ? \"wrap\" : \"nowrap\",\n        minWidth: 0,\n        ...style,\n      }}\n    >\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/status-chip.tsx"
    }
  ]
}
