{
  "name": "stage-shell",
  "type": "registry:ui",
  "title": "StageShell",
  "description": "The performance surface's one anatomy: header strip / main stage panel + 340px deck column / the console seat. The deck floats below lg, parks while the lens is open; the lens rides the Sheet contract.",
  "dependencies": [
    "@candlelight-ds/tokens",
    "clsx",
    "tailwind-merge",
    "lucide-react"
  ],
  "registryDependencies": [
    "sheet",
    "utils"
  ],
  "files": [
    {
      "path": "components/layouts/StageShell.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\n/**\n * StageShell — the performance surface's one anatomy (spec §4 Shells):\n * header strip / main stage panel + deck column / the console seat.\n * The performance surface runs it full-bleed; a rehearsal room runs it\n * inside the bench frame. \"Same layout\" is a fact of the import graph.\n *\n * The optional lens rides the Sheet contract: it slides in over the\n * main region's right edge under the scrim; ↑↓ keep walking the list\n * while it is up. While the lens is open the deck parks — the drawer\n * is the second panel, never a third.\n *\n * Use when: a surface IS the instrument (the show, the rehearsal).\n * Don't use when: list+detail editing (SplitShell) or single-panel\n * rooms (RoomShell).\n */\n\nimport { useState, type ReactNode } from 'react'\nimport { PanelRightOpen } from 'lucide-react'\n// The law number lives in ONE home (spec §4) — import, never re-declare.\nimport { STRIP_MIN_H } from '@candlelight-ds/tokens/metrics'\nimport { SheetRegion } from '@/components/ui/Sheet'\nimport { cn } from '@/lib/utils'\n\ninterface StageShellLens {\n  open: boolean\n  onClose: () => void\n  title: string\n  accent?: string\n  sheetKey?: string | number | null\n  width?: 340 | 380 | 440\n  /** Set a key and the lens can widen for real writing — one drawer\n   *  grammar in every room (Sheet postures, remembered per key). */\n  postureKey?: string\n  defaultPosture?: 'lens' | 'focus'\n  headerActions?: ReactNode\n  content: ReactNode\n}\n\ninterface StageShellProps {\n  /** Ambient light layer (your RoomGlow — C4's static room identity). */\n  glow?: ReactNode\n  /** Header strip, left: eyebrow · context. */\n  stripLeft: ReactNode\n  /** Header strip, right: status chips · actions. */\n  stripRight?: ReactNode\n  /** The stage panel's interior (the shell owns the panel chrome). */\n  stage: ReactNode\n  /** The deck column (340px; inline at lg+, floating above the stage\n   *  below lg so the list never becomes unreachable). Function form\n   *  receives the shell's hide control so the deck can seat the\n   *  dismiss button in its own header band. */\n  deck: ReactNode | ((controls: { hide: () => void }) => ReactNode)\n  /** Accessible label for the show-the-deck control. */\n  deckLabel?: string\n  /** The floating Sheet lens on the armed object. */\n  lens?: StageShellLens\n  /** The console seat (spec §7.a — the app's ONE console). */\n  console?: ReactNode\n  /** Overlays (help, pre-flight). */\n  children?: ReactNode\n  /** The performance surface runs full-bleed (h-dvh); rooms fill their frame. */\n  fullBleed?: boolean\n  /** The ambient posture (§2.6) — components under the shell read it\n   *  from the DOM, never via props. The stage is performance-density. */\n  density?: 'performance' | 'bench'\n}\n\nexport function StageShell({\n  glow,\n  stripLeft,\n  stripRight,\n  stage,\n  deck,\n  deckLabel = 'Show the list',\n  lens,\n  console: consoleSeat,\n  children,\n  fullBleed = false,\n  density = 'performance',\n}: StageShellProps) {\n  // While the lens is open the deck parks at EVERY width: the drawer is\n  // the second panel, never a third. It returns the moment the lens\n  // closes; ↑↓ keep walking the list while parked.\n  const deckParked = Boolean(lens?.open)\n\n  // The deck is dismissible: the stage can take the whole room.\n  const [deckOpen, setDeckOpen] = useState(true)\n\n  const strip = (\n    <header\n      data-optical /* a fixed band (§2.6a): its line centers optically */\n      className=\"relative z-rail flex items-center justify-between gap-4 px-7 pb-2 pt-5\"\n      style={{ minHeight: STRIP_MIN_H }}\n    >\n      <div className=\"flex min-w-0 items-baseline gap-3\">{stripLeft}</div>\n      {stripRight ? (\n        <div className=\"flex shrink-0 items-center gap-2.5\">{stripRight}</div>\n      ) : null}\n    </header>\n  )\n\n  const deckNode = typeof deck === 'function' ? deck({ hide: () => setDeckOpen(false) }) : deck\n\n  const grid = (\n    <div\n      className={cn(\n        'relative grid h-full min-h-0 grid-cols-1 gap-5',\n        deckOpen && !deckParked && 'lg:grid-cols-[minmax(0,1fr)_var(--col-deck)]'\n      )}\n    >\n      <div className=\"relative min-h-0 overflow-hidden rounded-md border border-hairline bg-surface/60 backdrop-blur-sm\">\n        {stage}\n      </div>\n      {deckOpen && !deckParked ? (\n        // The open deck exists at EVERY width: below lg it floats as a\n        // full-height panel above the stage's right edge (same 340px,\n        // same node — nothing teleports); at lg+ it is exactly the\n        // inline column.\n        <div className=\"min-h-0 absolute inset-y-0 end-0 z-float w-(--col-deck) max-w-full shadow-stage lg:static lg:inset-auto lg:z-auto lg:w-auto lg:shadow-none\">\n          {deckNode}\n        </div>\n      ) : null}\n    </div>\n  )\n\n  return (\n    <div\n      data-density={density}\n      className={cn(\n        'relative flex flex-col overflow-hidden bg-stage text-ink',\n        fullBleed ? 'h-dvh' : 'h-full'\n      )}\n    >\n      {glow}\n\n      {strip}\n\n      <main className=\"relative z-rail min-h-0 flex-1 px-5 pb-3 pt-2\">\n        {/* The way back lives WHERE THE DECK WAS — the same corner its\n            own hide button occupies, so the control never teleports. */}\n        {!deckOpen && !lens?.open ? (\n          <button\n            type=\"button\"\n            aria-label={deckLabel}\n            onClick={() => setDeckOpen(true)}\n            className=\"absolute end-9 top-6 z-rail flex h-7 w-7 items-center justify-center rounded-sm border border-hairline bg-surface/80 text-ink-mute backdrop-blur-sm transition-colors hover:bg-fill-hover hover:text-ink\"\n          >\n            <PanelRightOpen className=\"h-4 w-4\" />\n          </button>\n        ) : null}\n        {lens ? (\n          <SheetRegion\n            open={lens.open}\n            onClose={lens.onClose}\n            title={lens.title}\n            accent={lens.accent}\n            sheetKey={lens.sheetKey}\n            width={lens.width}\n            postureKey={lens.postureKey}\n            defaultPosture={lens.defaultPosture}\n            headerActions={lens.headerActions}\n            sheet={lens.content}\n          >\n            {grid}\n          </SheetRegion>\n        ) : (\n          grid\n        )}\n      </main>\n\n      {consoleSeat}\n      {children}\n    </div>\n  )\n}\n",
      "target": "components/layouts/StageShell.tsx"
    }
  ],
  "meta": {
    "candlelight": {
      "category": "shell",
      "gRules": [
        "G1",
        "G7",
        "C2",
        "C4"
      ],
      "useWhen": "A surface IS the instrument — the show, the rehearsal.",
      "dontUseWhen": "List+detail editing (SplitShell) or single-panel rooms (RoomShell).",
      "states": [
        "deck inline (lg+)",
        "deck floating (<lg)",
        "deck dismissed (stage takes the room)",
        "deck parked (lens open — the drawer is the second panel, never a third)",
        "full-bleed vs framed"
      ],
      "keyboard": [
        {
          "keys": "Esc (in the lens)",
          "does": "steps focus → lens → closed (Sheet contract)"
        }
      ],
      "acceptanceTests": [
        "The show-the-deck control lives where the deck's own hide button was — controls never teleport.",
        "↑↓ keep walking the list while the lens is up or the deck is parked."
      ]
    }
  }
}