{
  "name": "console",
  "type": "registry:ui",
  "title": "Console",
  "description": "The persistent docked control strip at the app frame's bottom edge — not a media player but the app's hands. Six zones: identity, transport+commit, the board, panic, level, status. Exactly ONE per product.",
  "dependencies": [
    "@candlelight-ds/tokens",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "transport-button",
    "commit-key",
    "status-dot",
    "utils"
  ],
  "files": [
    {
      "path": "components/instruments/Console.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\n/**\n * Console — the persistent docked control strip at the app frame's\n * bottom edge, present on every surface (spec §7.a). Not a media player\n * but the app's hands: whatever your product's RUNNING THING is (audio,\n * a timer, a deploy, a broadcast), this is where it stays visible and\n * controllable without navigating.\n *\n * THE ONE-INSTRUMENT LAW (non-negotiable): the product has exactly ONE\n * console. Bench and performance postures render the same components\n * reading the same store — they may differ in density (data-density)\n * and in which verbs are present, never in implementation. This\n * component warns in development if it finds a sibling console mounted.\n *\n * Six zones, left to right:\n *   identity — what is running: name, one line of state, the glow (G7).\n *              Identity only; it never navigates while something is live.\n *              First to hide when width tightens.\n *   transport — the running thing's verbs (TransportButtons) and, on the\n *              performance posture, the CommitKey with the staged name.\n *              Skip is honest: it walks a real queue or it sits dark (G2).\n *   board    — the flexible center (Pads / quick actions). Compresses to\n *              fit before anything clips; only then scrolls.\n *   panic    — the room-level toggles never more than a glance away.\n *              Toggles wear pressed (sunken); only the stop family wears\n *              danger.\n *   level    — the master fader (live onChange, onCommit on release).\n *   status   — StatusDots (G6) and the `?` key.\n *\n * The announcer line (G5) mounts immediately above the housing — pass it\n * through `announcer`; the console is its anchor on every surface.\n *\n * Use when: the app shell's one console slot — dock it at the frame's\n * bottom edge the way you mount nav.\n * Don't use when: anywhere else. A second \"footer\" is a failing test.\n */\n\nimport { useEffect, useRef } from 'react'\nimport { cn } from '@/lib/utils'\n\ninterface ConsoleProps {\n  /** Zone 1 — the identity cell. Omit it and the console still teaches:\n   *  the empty line renders in your product's voice. */\n  identity?: React.ReactNode\n  /** The empty-state teaching line (G5 voice) when nothing is running. */\n  emptyLine?: string\n  /** Zone 2 — transport verbs + the commit key. */\n  transport?: React.ReactNode\n  /** Zone 3 — the board (pads / quick actions). */\n  board?: React.ReactNode\n  /** Zone 4 — the panic row. STOP lives here, in one glance, always. */\n  panic?: React.ReactNode\n  /** Zone 5 — the master level. */\n  level?: React.ReactNode\n  /** Zone 6 — StatusDots and the ? key. */\n  status?: React.ReactNode\n  /** The announcer line — anchored above the housing (G5). */\n  announcer?: React.ReactNode\n  /** Copy (§9): the landmark's name — your metaphor's word for it. */\n  label?: string\n  className?: string\n}\n\nexport function Console({\n  identity,\n  emptyLine = 'Nothing running — the room is quiet.',\n  transport,\n  board,\n  panic,\n  level,\n  status,\n  announcer,\n  label = 'Console',\n  className,\n}: ConsoleProps) {\n  const ref = useRef<HTMLElement>(null)\n\n  // The one-instrument law, enforced where it can be: two mounted\n  // consoles is a bug even when they look identical (spec §7.a test 1).\n  useEffect(() => {\n    if (process.env.NODE_ENV === 'production') return\n    const consoles = document.querySelectorAll('[data-candlelight-console]')\n    if (consoles.length > 1) {\n      console.warn(\n        `[candlelight] ${consoles.length} consoles are mounted. The product has exactly ONE console (spec §7.a) — render the same Console from every posture, reading the same store.`\n      )\n    }\n  }, [])\n\n  return (\n    // Docked, not floating: flat surface, hairline-top, no shadow, no\n    // radius, no blur. Floating chrome means Overlay/Sheet; the console\n    // is furniture.\n    <footer\n      ref={ref}\n      data-candlelight-console\n      aria-label={label}\n      // pb-[env(...)]: the docked edge respects the device safe area —\n      // the one sanctioned arbitrary vertical pad (non-numeric by law).\n      className={cn(\n        '@container/console relative z-rail hairline-t pb-[env(safe-area-inset-bottom)]',\n        className\n      )}\n    >\n      {announcer}\n      {/* Density (§2.6) is ambient: one console, both postures — the\n          housing compacts under a bench ancestor; the keys inside\n          (TransportButton, CommitKey, Pad) read the same attribute. */}\n      <div className=\"flex items-center gap-4 bg-surface px-4 py-3 [[data-density=bench]_&]:gap-3 [[data-density=bench]_&]:px-3 [[data-density=bench]_&]:py-2\">\n        {/* Identity — fixed width, identity only; first zone to yield.\n            A CONTAINER query, not a viewport one: the console yields to\n            its own strip's width, so it stays honest docked in any frame. */}\n        <div className=\"hidden w-56 shrink-0 @frame/console:block\">\n          {identity ?? (\n            <p className=\"truncate text-body text-ink-faint\">{emptyLine}</p>\n          )}\n        </div>\n\n        {transport ? (\n          <div className=\"flex shrink-0 items-center gap-1.5\">{transport}</div>\n        ) : null}\n\n        {/* The board — compresses before anything clips; only a strip\n            too small for the full set falls back to hidden-scrollbar\n            horizontal scroll. The inner w-max + mx-auto pair centers a\n            fitting board but scrolls from the leading edge when it\n            overflows — justify-center would clip the first pads\n            unreachably. */}\n        {board ? (\n          <div className=\"flex min-w-0 flex-1 items-center border-x border-hairline px-4\">\n            <div className=\"w-full min-w-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n              <div className=\"mx-auto w-max\">{board}</div>\n            </div>\n          </div>\n        ) : (\n          <div className=\"min-w-0 flex-1\" />\n        )}\n\n        {panic ? (\n          <div className=\"flex shrink-0 items-center gap-1.5\">{panic}</div>\n        ) : null}\n\n        {(level || status) ? (\n          <div className=\"flex shrink-0 items-center gap-2.5 ps-1\">\n            {level}\n            {status}\n          </div>\n        ) : null}\n      </div>\n    </footer>\n  )\n}\n\n/** The `?` key — every surface's key map lives one press away (G2). */\nexport function ConsoleHelpKey({ onClick, label = 'Keys' }: { onClick: () => void; label?: string }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      aria-label={label}\n      className=\"flex h-7 w-7 items-center justify-center rounded-sm border border-hairline text-eyebrow text-ink-faint transition-colors hover:border-hairline-strong hover:text-ink\"\n    >\n      ?\n    </button>\n  )\n}\n",
      "target": "components/instruments/Console.tsx"
    }
  ],
  "meta": {
    "candlelight": {
      "category": "instrument",
      "gRules": [
        "G2",
        "G5",
        "G6",
        "G7"
      ],
      "useWhen": "The app shell's one console slot — dock it at the frame's bottom edge the way you mount nav. Whatever your product's running thing is lives here.",
      "dontUseWhen": "Anywhere else. A second 'footer' component is a failing test. Per-surface controls belong to the surface.",
      "states": [
        "running (identity + glow)",
        "empty (teaches, in voice)",
        "identity hidden (width yields)",
        "board compressed",
        "board scrolling"
      ],
      "keyboard": [
        {
          "keys": "?",
          "does": "opens the surface's generated key map (ConsoleHelpKey)"
        }
      ],
      "traps": [
        "Two strips that 'look the same' but share no code will drift within a month — this law exists because it happened. The component warns in dev when two consoles mount."
      ],
      "acceptanceTests": [
        "The one-instrument test: exactly one implementation, imported by every posture; the store is shared. (Dev warning fires on a second mount.)",
        "The glance test: from any surface, one glance at the bottom edge answers what is running, is it healthy, how do I stop it.",
        "The navigation test: nothing in the console navigates away while something is live.",
        "The honesty test: every printed key fires; every control that can't act sits dark rather than lying.",
        "The panic test: STOP is reachable in one click from every surface, wears danger, never moves between postures.",
        "The empty test: with nothing running the console still teaches, in voice, and the commit key shows what's staged."
      ]
    }
  }
}