{
  "name": "candlelight",
  "homepage": "https://candlelight-docs.vercel.app",
  "items": [
    {
      "name": "kbd",
      "type": "registry:ui",
      "title": "Kbd",
      "description": "The printed key. Any action used more than twice per session shows its key on the control. Renders ⌘ vs Ctrl by platform.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Kbd.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G2"
          ],
          "useWhen": "A control has a keyboard binding.",
          "dontUseWhen": "Explaining a concept — that's Tooltip's job.",
          "states": [
            "default",
            "brass",
            "onBrass",
            "xs",
            "sm"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "Never render a Kbd for a binding that doesn't fire (honest printed keys).",
            "'mod' renders ⌘ on Apple platforms, Ctrl elsewhere."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "button",
      "type": "registry:ui",
      "title": "Button",
      "description": "Every click that isn't a row or a pad. Machined edge, 1px of real travel; strength is structure, never glow.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "class-variance-authority",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "kbd",
        "utils"
      ],
      "files": [
        {
          "path": "components/ui/Button.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G2",
            "G7",
            "C3"
          ],
          "useWhen": "Forms, headers, overlays, empty-state actions.",
          "dontUseWhen": "Persistent surface controls (TransportButton), one-shots (Pad), or status tokens (Chip).",
          "states": [
            "primary",
            "destructive",
            "outline",
            "secondary",
            "quiet",
            "link",
            "disabled",
            "with printed key"
          ],
          "keyboard": [
            {
              "keys": "Enter/Space",
              "does": "activates (native button)"
            }
          ],
          "acceptanceTests": [
            "Exactly one primary per view region (C3).",
            "Every variant travels 1px on press.",
            "Destructive wears the cold danger tone — red never invites."
          ]
        }
      }
    },
    {
      "name": "chip",
      "type": "registry:ui",
      "title": "Chip",
      "description": "Tiny status/count/label tokens: live (pulsing dot), armed, ready, warn, danger, count, tag. Absorbs every bespoke pill.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "class-variance-authority",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Chip.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G6",
            "G7"
          ],
          "useWhen": "Marking state (live/armed/ready), counts, or tags on rows and headers.",
          "dontUseWhen": "The element triggers a primary action (Button) or fires a one-shot (Pad).",
          "states": [
            "live",
            "armed",
            "quiet",
            "ready",
            "warn",
            "danger",
            "count",
            "tag",
            "with dot",
            "with pulsing dot"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "The live pulse dies under prefers-reduced-motion.",
            "live and armed are different hues — the eye separates staged from live without reading."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "tooltip",
      "type": "registry:ui",
      "title": "Tooltip",
      "description": "The explanation layer — one line of words on a 400ms delay. The Kbd chip carries the key; this carries the words. title= is lint-banned in its favor.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "@radix-ui/react-tooltip",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Tooltip.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G10"
          ],
          "useWhen": "An icon-only control needs a name, or a control needs one line of context.",
          "dontUseWhen": "The content is interactive (Menu/Overlay) or essential to completing the task (put it in the UI).",
          "states": [
            "closed",
            "open (400ms delay)",
            "disabled passthrough"
          ],
          "keyboard": [
            {
              "keys": "Focus",
              "does": "shows after delay; Esc hides"
            }
          ],
          "acceptanceTests": [
            "Mandatory on every icon-only button (with aria-label).",
            "One line of explanation, never the key itself."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "menu",
      "type": "registry:ui",
      "title": "Menu",
      "description": "ONE floating-menu engine for dropdown, right-click, and overflow actions: items, icons, printed keys, checkbox items, danger items, max one submenu level.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "@radix-ui/react-dropdown-menu",
        "@radix-ui/react-context-menu",
        "lucide-react",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "kbd",
        "utils"
      ],
      "files": [
        {
          "path": "components/ui/Menu.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G2",
            "G9"
          ],
          "useWhen": "Contextual actions on an object — dropdown, right-click, or overflow.",
          "dontUseWhen": "Navigation (links) or picking data values (Select). Deeper than one submenu level (Overlay).",
          "states": [
            "closed",
            "open",
            "item highlighted",
            "item disabled",
            "danger item",
            "checkbox item",
            "submenu open"
          ],
          "keyboard": [
            {
              "keys": "↑↓",
              "does": "walk items"
            },
            {
              "keys": "Enter",
              "does": "select"
            },
            {
              "keys": "Esc",
              "does": "close"
            },
            {
              "keys": "type",
              "does": "typeahead"
            }
          ],
          "acceptanceTests": [
            "A second menu engine anywhere is a bug — dropdown and context modes share this one skin.",
            "closeOnSelect:false keeps the menu open for rapid-fire actions.",
            "Danger items wear the cold tone and pair with hold-to-strike for destruction."
          ]
        }
      }
    },
    {
      "name": "overlay",
      "type": "registry:ui",
      "title": "Overlay",
      "description": "The one mount above z-float: modal (460/640px) or bare. Focus trap, Esc, scroll lock, stage shadow, scale-in. Overlays never stack two deep.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "@radix-ui/react-dialog",
        "lucide-react",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Overlay.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G1"
          ],
          "useWhen": "The user must address something before continuing on this surface — a blocking decision.",
          "dontUseWhen": "Editing an object adjacent to a list (Sheet) or a whole workflow (a route). Modal is the last resort, not the first thought.",
          "states": [
            "closed",
            "open sm (460px)",
            "open lg (640px)",
            "bare",
            "locked (typed-confirm)"
          ],
          "keyboard": [
            {
              "keys": "Esc",
              "does": "closes (unless locked)"
            },
            {
              "keys": "Tab",
              "does": "cycles inside the trap"
            }
          ],
          "acceptanceTests": [
            "Overlays never stack two deep — a modal needing a picker uses an inline combobox.",
            "Scrim is stage/80 with 2px blur; the panel wears shadow-stage."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "sheet",
      "type": "registry:ui",
      "title": "Sheet",
      "description": "The one drawer: floats over the region (never reflows the layout), full takeover under 700px, a lens on the armed object with crossfade swap, resizable handle with keyboard path, lens/focus postures remembered per key.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "lucide-react",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Sheet.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G1",
            "G7"
          ],
          "useWhen": "Editing or inspecting the thing selected from an adjacent list.",
          "dontUseWhen": "Blocking decisions (Overlay) or whole workflows (a route).",
          "states": [
            "closed",
            "open lens (340px)",
            "open focus (560px+)",
            "takeover (<700px container)",
            "dragging the handle",
            "content swapping (120ms crossfade)"
          ],
          "keyboard": [
            {
              "keys": "Esc",
              "does": "steps focus → lens → closed"
            },
            {
              "keys": "←/→ on the handle",
              "does": "nudges width 16px"
            }
          ],
          "traps": [
            "Cold-mount focus: focus the title only after the panel exists (key the effect on mounted, not just open).",
            "Focus steal: the title-focus effect must never depend on callback props — an unstable onClose would re-fire it on every continuous save."
          ],
          "acceptanceTests": [
            "The layout behind never reflows (push mode is dead).",
            "Arming a different object swaps content in place — the drawer does not close/reopen.",
            "The handle is a citizen: role=separator, focusable, ←/→ nudges, width remembered per key per posture.",
            "Closing is announced with Undo when local state dies, never blocked by a dialog."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "ink",
      "type": "registry:ui",
      "title": "Ink",
      "description": "The editing feel: there is no edit mode. One ProseMirror-backed primitive for every text seat — line/text/prose — with continuous save, real undo, Esc burst-revert, and [[ mentions.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "@tiptap/react",
        "@tiptap/starter-kit",
        "@tiptap/core",
        "@tiptap/extension-document",
        "@tiptap/extension-paragraph",
        "@tiptap/extension-text",
        "@tiptap/extensions",
        "tiptap-markdown",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "menu",
        "utils"
      ],
      "files": [
        {
          "path": "components/ui/Ink.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G8"
          ],
          "useWhen": "Any text the user owns — titles, names, descriptions, documents.",
          "dontUseWhen": "Genuine configuration (URLs, keys — forms stay forms) or search fields (queries, not documents). Create-inputs and renames under navigating links (InlineEdit).",
          "states": [
            "reading (identical to writing)",
            "writing (caret only)",
            "empty (placeholder)",
            "read-only (same face)",
            "mention popup open"
          ],
          "keyboard": [
            {
              "keys": "Enter (line)",
              "does": "commits and blurs — Enter stays sacred to the commit key"
            },
            {
              "keys": "Esc",
              "does": "reverts the current burst since the last save"
            },
            {
              "keys": "⌘Z",
              "does": "real undo, alive across saves"
            },
            {
              "keys": "⌘B/⌘I (prose)",
              "does": "bold/italic; markdown shortcuts free"
            },
            {
              "keys": "[[ (prose)",
              "does": "opens the mention popup; ↑↓ walk, Enter picks, Esc closes"
            }
          ],
          "traps": [
            "Suggestion-popup key priority: popup keys MUST be handled in editorProps.handleKeyDown — a React wrapper's onKeyDown runs after ProseMirror's keymaps, so Enter splits the paragraph instead of picking."
          ],
          "acceptanceTests": [
            "The squint test: a screenshot of editing vs reading is identical but the caret.",
            "The keystroke test: typing is indistinguishable from a bare textarea (zero React renders per keystroke).",
            "The crash test: kill the tab mid-word, lose at most one debounce window of words.",
            "The undo test: ⌘Z works across saves because the editor never unmounts.",
            "The menu test: no seat without a right-click menu.",
            "The one-feel test: a stranger can't tell any two seats were different components."
          ]
        }
      }
    },
    {
      "name": "inline-edit",
      "type": "registry:ui",
      "title": "InlineEdit",
      "description": "The commit-once exception to Ink: create-inputs and rename overlays inside navigating links, de-chromed to match Ink at rest. The input wears the display's exact box.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/InlineEdit.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G8"
          ],
          "useWhen": "Create-by-naming flows (typing then Enter creates) and renames overlaid on a link, where continuous save would create-on-idle.",
          "dontUseWhen": "Any text that lives — that's Ink.",
          "states": [
            "display (truncating block)",
            "editing (seamless input)",
            "empty (placeholder)"
          ],
          "keyboard": [
            {
              "keys": "Enter",
              "does": "commits once"
            },
            {
              "keys": "Esc",
              "does": "cancels, restores the display"
            },
            {
              "keys": "double-click",
              "does": "enters editing (optional)"
            }
          ],
          "acceptanceTests": [
            "No border, fill, or padding delta between display and input — the caret is the only mode.",
            "The display face is a block so truncation actually clips (overflow is inert on inline boxes)."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "slider",
      "type": "registry:ui",
      "title": "Slider",
      "description": "The level control with the onChange/onCommit split: live feel every movement, persist once on release — hardware and API consumers never flood.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "@radix-ui/react-slider",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Slider.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G3"
          ],
          "useWhen": "Setting a level — brightness, volume, temperature, intensity.",
          "dontUseWhen": "Picking among a few discrete options (SegmentedControl).",
          "states": [
            "resting",
            "dragging (live onChange)",
            "released (onCommit)",
            "disabled",
            "spectrum track"
          ],
          "keyboard": [
            {
              "keys": "←/→",
              "does": "steps the value"
            },
            {
              "keys": "Home/End",
              "does": "min/max"
            }
          ],
          "acceptanceTests": [
            "onChange fires every movement; onCommit exactly once on release.",
            "Flat two-zone progress paint — no gradient chrome (the spectrum track is content, not chrome)."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "segmented-control",
      "type": "registry:ui",
      "title": "SegmentedControl",
      "description": "2–4 visible exclusive options. More than 4 or an unknown count belongs in a Select/Combobox.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/SegmentedControl.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G1"
          ],
          "useWhen": "Switching between a few exclusive views or modes.",
          "dontUseWhen": "More than 4 options, unknown counts, or navigation.",
          "states": [
            "selected (fill-active)",
            "unselected (hover ramp)",
            "with counts"
          ],
          "keyboard": [
            {
              "keys": "click/tab",
              "does": "switches; selection is immediate (optimistic)"
            }
          ],
          "acceptanceTests": [
            "The selected segment sits on fill-active; the rest ride the hover ramp."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "skeleton",
      "type": "registry:ui",
      "title": "Skeleton",
      "description": "A flat fill breathing opacity (1 → 0.55, 1.8s) — no shimmer wash. Spinners are banned outside buttons.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Skeleton.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G5"
          ],
          "useWhen": "A pane is fetching content with a known rough shape.",
          "dontUseWhen": "An action button is pending — use its disabled state.",
          "states": [
            "breathing",
            "static under reduced motion"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "The breathe loop dies under prefers-reduced-motion.",
            "SkeletonRows carries role=status aria-label for screen readers."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "announcer",
      "type": "registry:ui",
      "title": "Announcer + Toast",
      "description": "G5 embodied: announce() renders the one quiet transient line per surface region (Undo lives in the line); the toast viewport is for async outcomes only, max two.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "zustand",
        "lucide-react",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/ui/Announcer.tsx",
          "type": "registry:ui"
        },
        {
          "path": "components/ui/Toast.tsx",
          "type": "registry:ui"
        },
        {
          "path": "hooks/useAnnouncer.ts",
          "type": "registry:hook"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G4",
            "G5"
          ],
          "useWhen": "Announcer: confirming a direct manipulation. Toast: an async job finished or failed while the user was elsewhere.",
          "dontUseWhen": "Celebration toasts for direct actions — the change itself is the confirmation.",
          "states": [
            "announcement visible (5.5s wall)",
            "announcement with Undo",
            "toast ready/danger/warn/info",
            "two toasts (the cap)"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "One announcement at a time, in a fixed spot, in voice.",
            "Max two toasts; the oldest yields.",
            "Every destructive outcome carries Undo."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "empty-state",
      "type": "registry:ui",
      "title": "EmptyState",
      "description": "Teaches the surface's verb, in voice; never 'nothing here.' Two variants: panel and inline.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "button",
        "utils"
      ],
      "files": [
        {
          "path": "components/ui/EmptyState.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [
            "G5"
          ],
          "useWhen": "A list or region has no content yet and there is a next verb to teach.",
          "dontUseWhen": "Content is loading (Skeleton) or an error needs naming.",
          "states": [
            "panel",
            "inline",
            "with action"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "The title is a verb in the product's voice, never 'No items found.'"
          ]
        }
      }
    },
    {
      "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"
        }
      ],
      "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."
          ]
        }
      }
    },
    {
      "name": "transport-button",
      "type": "registry:ui",
      "title": "TransportButton",
      "description": "A persistent surface control: icon · micro-label · printed key · pressed tone. Density-aware — compacts to a 32px icon under data-density=\"bench\". Held keys sit physically sunken.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "tooltip",
        "utils"
      ],
      "files": [
        {
          "path": "components/instruments/TransportButton.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G2",
            "G7"
          ],
          "useWhen": "Always-visible surface-level controls — console zones, transport rows.",
          "dontUseWhen": "One-off form/header actions (Button) or one-shots (Pad).",
          "states": [
            "resting",
            "pressed/held (sunken)",
            "danger pressed (the stop family)",
            "disabled (dark, honest)",
            "bench density (icon only)"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "hotkey is optional — never print a key that doesn't work (G2).",
            "Only the stop family wears danger.",
            "A held toggle sits sunken (edge-sunken), not tinted."
          ]
        }
      }
    },
    {
      "name": "commit-key",
      "type": "registry:ui",
      "title": "CommitKey",
      "description": "The wide commit control for G1 (the GO family): dark console key, strong accent rim, lit legend, machined edge, 1px travel. Armed breathes the rim's temperature. Live is not a button — it becomes the status token.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "kbd",
        "utils"
      ],
      "files": [
        {
          "path": "components/instruments/CommitKey.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G1",
            "G2",
            "G6",
            "G7"
          ],
          "useWhen": "Committing the armed object — GO, TAKE, Apply, Assign. Exactly one per surface.",
          "dontUseWhen": "Ordinary confirmations (Button variant=\"primary\").",
          "states": [
            "idle",
            "armed (rim breathes — color pulse, never halo)",
            "live (role=status: pulsing dot, no click, no printed key)",
            "disabled",
            "sm (compact cut)"
          ],
          "keyboard": [
            {
              "keys": "⏎",
              "does": "the printed default — Enter is sacred to the commit key (§5)"
            }
          ],
          "acceptanceTests": [
            "Live renders role=status with no button and no Kbd.",
            "The armed pulse is border-color only and dies under reduced motion (key-cue-armed)."
          ]
        }
      }
    },
    {
      "name": "pad",
      "type": "registry:ui",
      "title": "Pad",
      "description": "A one-shot trigger in a grid: fire-and-forget, never touches transport state. The strike squishes (scale 0.94) and blooms the accent, decaying like a drum-pad LED, restartable mid-decay.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/instruments/Pad.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G2",
            "G3"
          ],
          "useWhen": "Firing a one-shot over the bed — a sting, a stinger, a flash.",
          "dontUseWhen": "Anything stateful (TransportButton) or commits (CommitKey).",
          "states": [
            "resting",
            "hover (accent warm-up)",
            "striking (pad-hit bloom)",
            "board size (h-20 grid cell)"
          ],
          "keyboard": [
            {
              "keys": "printed hotkey",
              "does": "fires via the surface map; bump hitSeq so the pad flashes for the keyboard hand too"
            }
          ],
          "acceptanceTests": [
            "Rapid fire reads as rapid fire: the bloom restarts mid-decay (remove class → reflow → re-add).",
            "The bloom dies under prefers-reduced-motion."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "armed-row",
      "type": "registry:ui",
      "title": "ArmedRow",
      "description": "The list row that composes the grammar: arm ring, live chip, readiness dot, in-place rename, hover-revealed actions, context menu, reorder handle.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "ink",
        "menu",
        "tooltip",
        "chip",
        "status-dot",
        "utils"
      ],
      "files": [
        {
          "path": "components/instruments/ArmedRow.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G1",
            "G3",
            "G6",
            "G7",
            "G8",
            "G9",
            "G10"
          ],
          "useWhen": "Rows in a SplitShell/StageShell list that arm an object for editing or performing.",
          "dontUseWhen": "Multi-select tables (compose useSelection's styling contract yourself) or uniform archive rows (C5).",
          "states": [
            "resting",
            "hover (actions reveal)",
            "armed (fill-active + accent ring)",
            "live (fill-selected + chip)",
            "renaming (Ink line seat)",
            "present-tense accent rule"
          ],
          "keyboard": [
            {
              "keys": "click / ↑↓ via surface map",
              "does": "arms — staging, never execution (G1)"
            },
            {
              "keys": "R via surface map",
              "does": "flips `editing` — the Ink seat selects all"
            }
          ],
          "acceptanceTests": [
            "Clicking the row arms; it never plays (G3 — the audition trigger is explicit).",
            "Hover reveals affordances or nothing — no dead hover (G10)."
          ]
        }
      }
    },
    {
      "name": "status-dot",
      "type": "registry:ui",
      "title": "StatusDot + ReadinessDot",
      "description": "G6 embodied: status is a dot until it isn't. StatusDot for ambient system state (click expands to detail); ReadinessDot for per-object readiness in rows.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "tooltip",
        "utils"
      ],
      "files": [
        {
          "path": "components/instruments/StatusDot.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G6"
          ],
          "useWhen": "Glanceable state that should not shout — connection, sync, per-row readiness.",
          "dontUseWhen": "The state demands immediate action (Announcer/Toast) — no persistent status bars, no preemptive banners.",
          "states": [
            "ok",
            "warn (pulse)",
            "off",
            "busy (pulse)",
            "ready/partial/failed/pending (readiness)"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "Every dot carries a text equivalent (tooltip + aria-label) that names the state, not the color.",
            "Pulses die under reduced motion."
          ]
        }
      }
    },
    {
      "name": "hold-to-strike",
      "type": "registry:ui",
      "title": "HoldToStrike",
      "description": "G4 as a behavior: hold ~600ms while the fill sweeps, release early cancels, completion strikes — and the CALLER announces with Undo. A hook for any control plus the ready-made StrikeButton.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/instruments/HoldToStrike.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G4",
            "G5"
          ],
          "useWhen": "Deleting one thing — a row, an object, a pad. Wrap menu items and icon buttons with the hook.",
          "dontUseWhen": "Cascade deletes — those earn a typed-confirm Overlay.",
          "states": [
            "resting (cold danger outline)",
            "holding (linear fill sweep)",
            "released early (sweep snaps back)",
            "struck"
          ],
          "keyboard": [
            {
              "keys": "Enter/Space held",
              "does": "same hold; keyup cancels — the keyboard path is first-class"
            }
          ],
          "acceptanceTests": [
            "Release before the threshold never strikes.",
            "Completion fires exactly once and suppresses the trailing click.",
            "Every strike is announced with Undo at the call site — undo is a deferred commit, not a restore."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "intensity-bands",
      "type": "registry:ui",
      "title": "IntensityBands",
      "description": "The 1–N stepped state selector with printed number keys: the same scene, hotter. Band names are your metaphor's words; the number row fires them.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "kbd",
        "utils"
      ],
      "files": [
        {
          "path": "components/instruments/IntensityBands.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G1",
            "G2",
            "G7"
          ],
          "useWhen": "Stepping one dimension of the live thing through named levels (Ambient→Resolve, Energy 1–4).",
          "dontUseWhen": "Continuous values (Slider) or exclusive views (SegmentedControl — bands are an instrument, not a filter).",
          "states": [
            "inactive bands",
            "active band (lit + glow — this IS what is live)",
            "the hot band (ember)",
            "disabled"
          ],
          "keyboard": [
            {
              "keys": "1…N",
              "does": "fires the band via the surface map; the keys are printed on the control"
            }
          ],
          "acceptanceTests": [
            "The number keys print from the band order — help cannot drift (G2)."
          ]
        }
      }
    },
    {
      "name": "flames",
      "type": "registry:ui",
      "title": "Flames",
      "description": "Small animated bars proving activity is flowing. CSS grade for list rows (cheap, many); signal-driven grade whose bar heights follow a real level.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/instruments/Flames.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G7"
          ],
          "useWhen": "Something is audibly/visibly running — a track, a job, a broadcast.",
          "dontUseWhen": "Decoration. Flames mean activity; an idle state never wears them.",
          "states": [
            "active (three CSS bars)",
            "signal-driven (scaleY follows the level)",
            "hidden when idle",
            "static under reduced motion"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "aria-hidden — flames are a visual double of state that exists elsewhere in text.",
            "The CSS loop dies under prefers-reduced-motion (flame-bar)."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "glow-panel",
      "type": "registry:ui",
      "title": "GlowPanel",
      "description": "The per-card accent glow: a panel glows with its object's accent while that object is happening, and goes quiet otherwise. Glow is state, never decoration.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/instruments/GlowPanel.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "instrument",
          "gRules": [
            "G7",
            "C4"
          ],
          "useWhen": "A card/cell represents something currently live or armed — the identity cell, a live card.",
          "dontUseWhen": "Decoration, or app-level ambient tinting (that's the room-glow layer: two radial washes at 2.5s, a StageShell glow slot).",
          "states": [
            "quiet (flat accent tint)",
            "active (radial glow + soft shadow)"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "The glow gradient is the one sanctioned gradient outside content — it may appear only while active."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "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"
        }
      ],
      "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."
          ]
        }
      }
    },
    {
      "name": "room-shell",
      "type": "registry:ui",
      "title": "RoomShell",
      "description": "Every bench room: one floating strip (min-height 76px: eyebrow · context left, verbs right) over ONE stage panel. Rooms are not allowed bespoke anatomies.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "components/layouts/RoomShell.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "shell",
          "gRules": [
            "C1",
            "C4",
            "C6"
          ],
          "useWhen": "A bench room — the shelf, the library, settings.",
          "dontUseWhen": "The instrument itself (StageShell) or list+detail (SplitShell).",
          "states": [
            "default strip (eyebrow + verbs)",
            "custom strip (toolbar rooms)"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "'Looks like the performance surface' is a fact of the import graph — the strip grammar and panel chrome are shared exports (RoomEyebrow, RoomPanel)."
          ]
        }
      },
      "registryDependencies": [
        "utils"
      ]
    },
    {
      "name": "split-shell",
      "type": "registry:ui",
      "title": "SplitShell",
      "description": "List + adjacent editor with the floating Sheet built into the detail region. Rows are ArmedRow compositions; selection↔URL stays with the caller.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "sheet",
        "utils"
      ],
      "files": [
        {
          "path": "components/layouts/SplitShell.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "shell",
          "gRules": [
            "G1"
          ],
          "useWhen": "A list of objects drives an adjacent editor.",
          "dontUseWhen": "A table IS the surface, or full-bleed performance surfaces (StageShell).",
          "states": [
            "list + detail",
            "sheet floating over the detail region"
          ],
          "keyboard": [],
          "acceptanceTests": [
            "The sheet floats over the DETAIL region only — the list keeps its scroll and its width."
          ]
        }
      }
    },
    {
      "name": "use-surface-keyboard",
      "type": "registry:hook",
      "title": "useSurfaceKeyboard",
      "description": "One window keydown listener per surface, declared as a binding map. Bindings are ignored while typing unless whileTyping; the list doubles as the source the ? key map renders from — help cannot drift from reality.",
      "dependencies": [],
      "files": [
        {
          "path": "hooks/useSurfaceKeyboard.ts",
          "type": "registry:hook"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "hook",
          "gRules": [
            "G2"
          ],
          "useWhen": "A surface answers to keys — arm/commit/rename/strike, ⌘K, undo, the number row.",
          "dontUseWhen": "A focused widget should handle its own keys (Ink, Menu, InlineEdit) — those swallow events before this hook sees them.",
          "states": [],
          "keyboard": [
            {
              "keys": "↑↓ ⏎ Esc Space R ⌘K ⌘Z ? 1–9",
              "does": "the grammar-reserved keys every surface declares here (§5)"
            }
          ],
          "traps": [
            "Bare window.addEventListener('keydown') outside this hook is lint-banned (@candlelight-ds/no-bare-keydown) — this file carries the one sanctioned disable."
          ],
          "acceptanceTests": [
            "Typing targets swallow surface keys unless the binding says whileTyping.",
            "Undeclared shift on a letter key is a different intent (R ≠ shift+R).",
            "The ? overlay renders from this binding list, never from hand-written help."
          ]
        }
      }
    },
    {
      "name": "use-selection",
      "type": "registry:hook",
      "title": "useSelection",
      "description": "The ONE selection model: single/multi over an ordered id list, keyboard focus cursor, shift-range, auto-prune. Selection is staging, never execution.",
      "dependencies": [],
      "files": [
        {
          "path": "hooks/useSelection.ts",
          "type": "registry:hook"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "hook",
          "gRules": [
            "G1"
          ],
          "useWhen": "Any list where rows can be armed or checked.",
          "dontUseWhen": "A single persistent 'active' object owned by a store (the live thing) — that is the running store's state, not list selection.",
          "states": [
            "selected → bg-fill-selected",
            "focused cursor → inset hairline ring",
            "neither → hover ramp on transparent rest"
          ],
          "keyboard": [
            {
              "keys": "↑↓ / shift-click / ⌘A",
              "does": "wire focusDelta, selectRange, toggleAll through the surface map"
            }
          ],
          "acceptanceTests": [
            "Selection prunes automatically when the list changes (filter, sort, strike) without breaking set identity.",
            "The styling contract is decided once, here — rows read it, never restyle it."
          ]
        }
      }
    },
    {
      "name": "use-drag-reorder",
      "type": "registry:hook",
      "title": "useDragReorder",
      "description": "ONE reorder behavior for visible lists: HTML5 drag with a private MIME type (card drags never trigger file-drop zones), axis-free drop edges, and the keyboard path (moveBy ±1, bind ⌥↑/⌥↓).",
      "dependencies": [],
      "files": [
        {
          "path": "hooks/useDragReorder.ts",
          "type": "registry:hook"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "hook",
          "gRules": [
            "G9"
          ],
          "useWhen": "Reordering rows/cards within one visible list.",
          "dontUseWhen": "Moving items BETWEEN collections (picker/⌘K, per G9) or file-drop zones.",
          "states": [
            "dragging",
            "drop target before/after edge"
          ],
          "keyboard": [
            {
              "keys": "⌥↑/⌥↓",
              "does": "moveBy(id, ±1) — the a11y path is the same code path"
            }
          ],
          "acceptanceTests": [
            "A file drag never reorders; a card drag never opens a drop zone (private MIME).",
            "The drop edge follows the axis the two items actually differ on — rail, list, and grid share one hook."
          ]
        }
      }
    },
    {
      "name": "utils",
      "type": "registry:lib",
      "title": "utils (cn)",
      "description": "The class combiner every component leans on: clsx + tailwind-merge with proper precedence. Ships automatically as a registry dependency of every component.",
      "dependencies": [
        "clsx",
        "tailwind-merge"
      ],
      "files": [
        {
          "path": "lib/utils.ts",
          "type": "registry:lib"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "hook",
          "gRules": [],
          "useWhen": "Composing conditional Tailwind classes in any registry component.",
          "dontUseWhen": "Anywhere else — it is plumbing, not a pattern.",
          "states": [],
          "keyboard": [],
          "acceptanceTests": [
            "Later classes win conflicts (twMerge precedence), so a consumer's className prop can override a component default."
          ],
          "traps": [
            "tailwind-merge cannot tell a renamed font-size (text-marquee) from a text color (text-ink) — both wear text-. The shipped cn registers the named type steps via extendTailwindMerge; replace it with a bare twMerge and cn('text-marquee', 'text-ink') silently drops the size."
          ]
        }
      }
    },
    {
      "name": "grid-overlay",
      "type": "registry:ui",
      "title": "GridOverlay",
      "description": "The baseline grid made visible (§2.6a): minor line every 4px baseline, major every body leading. Governance, not chrome — a grid you can't toggle on and measure is a mood board.",
      "dependencies": [
        "@candlelight-ds/tokens",
        "clsx",
        "tailwind-merge"
      ],
      "registryDependencies": [
        "utils"
      ],
      "files": [
        {
          "path": "components/ui/GridOverlay.tsx",
          "type": "registry:ui"
        }
      ],
      "meta": {
        "candlelight": {
          "category": "primitive",
          "gRules": [],
          "useWhen": "Proving a surface's vertical rhythm in review — every leading, control height, and band metric on the baseline.",
          "dontUseWhen": "Decorating. Ship it behind a dev/review gate; the grid is governance, not chrome.",
          "states": [
            "hidden",
            "open"
          ],
          "keyboard": [
            {
              "keys": "G",
              "does": "toggle the overlay (bind it in your surface map so it prints in the help)"
            }
          ],
          "traps": [
            "Mount the overlay INSIDE the surface it audits — same content box, a relative parent — never as a full-width sibling. An overlay in a different box drifts from the content at some viewport width and lies about alignment."
          ],
          "acceptanceTests": [
            "Every text step's bundled line-height is a whole multiple of --baseline (the tokens package law-tests this).",
            "The overlay renders nothing when closed and is aria-hidden + pointer-events-none when open."
          ]
        }
      }
    }
  ]
}