{
  "name": "CollapsiblePanel",
  "type": "pattern",
  "description": "A titled section that opens and closes, with a +/- cross-fade toggle",
  "export": "CollapsiblePanel",
  "path": "@gtmi/strix-react/organisms/collapsible-panel",
  "props": [
    {
      "name": "title",
      "type": "ReactNode",
      "required": true,
      "description": "Header content, rendered inside the trigger"
    },
    {
      "name": "titleAs",
      "type": "h2 | h3 | h4 | h5 | h6",
      "description": "Heading level wrapped around the title. Set it whenever the panel is part of a document outline — a section with no heading is invisible to a screen reader's heading navigation"
    },
    {
      "name": "subtitle",
      "type": "ReactNode",
      "description": "Secondary line under the title"
    },
    {
      "name": "open",
      "type": "boolean",
      "description": "Controlled open state. Pair with onOpenChange"
    },
    {
      "name": "defaultOpen",
      "type": "boolean",
      "default": "true",
      "description": "Initial state when uncontrolled"
    },
    {
      "name": "onOpenChange",
      "type": "(open: boolean) => void",
      "description": "Called with the next open state on every toggle, controlled or not"
    },
    {
      "name": "hideToggle",
      "type": "boolean",
      "default": "false",
      "description": "Hide the toggle and hold the panel open. The header stops being a button, so nothing focusable announces an interaction that cannot happen"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "description": "Prevent toggling while keeping the toggle visible and announced as disabled"
    },
    {
      "name": "contentClassName",
      "type": "string",
      "description": "Applied to the body wrapper rather than the panel root"
    }
  ],
  "parts": [
    {
      "name": "collapsible-panel",
      "description": "Root, via data-slot"
    },
    {
      "name": "collapsible-panel-header",
      "description": "The trigger, or a plain div when hideToggle is set"
    },
    {
      "name": "collapsible-panel-toggle",
      "description": "The +/- glyph wrapper, aria-hidden"
    },
    {
      "name": "collapsible-panel-content",
      "description": "The body, always mounted"
    }
  ],
  "ai_tags": ["collapsible", "disclosure", "panel", "accordion", "expandable", "pattern"],
  "since": "0.2.0",
  "source": "cr-web-demo ConfigDropdowns and ProfileDropdowns",
  "tokens_used": [
    "--color-surface-panel",
    "--color-border-panel",
    "--color-border-focus",
    "--color-text-primary",
    "--color-text-secondary",
    "--color-text-accent-strong",
    "--color-text-disabled",
    "--radius-large",
    "--spacing-gap-050",
    "--spacing-gap-300",
    "--spacing-padding-horizontal-md",
    "--spacing-padding-vertical-md",
    "--text-size-subhead-small"
  ],
  "notes": "Built on Base UI Collapsible, not the sources' hand-rolled boolean, so it carries aria-expanded, aria-controls, trigger/panel id wiring and Space/Enter activation. Neither source has any of that; ConfigDropdowns's toggle is a bare <button> with no ARIA. Three things deliberately not ported: conversationState auto-collapse (the app's policy, and implemented upstream via an effect that writes state during render), the hand-rolled controlled-or-uncontrolled resolution, and the unmounting body — this keeps the body mounted and animates grid-template-rows so in-page search and tests can find it. Both transitions carry motion-reduce:transition-none per WCAG 2.3.3; neither animation conveys information the state attributes do not. TOKEN CHOICE: cr-web-demo paints the +/- glyph #3acefa, which measures 1.84:1 against the light page — --color-text-accent-strong is 6.98:1 light and 12.12:1 dark. RADIUS: 12px, not Card's 16px. cr-web-demo runs a three-step hierarchy — 16px outer page shell (rounded-2xl on AgentPanel), 12px panel nested inside one (rounded-xl, both sources), 8px control (rounded-lg on ConfigSelect) — so Card's shell radius made a nested panel read as top-level. Contrast is asserted at 4.5:1 in both themes from tokens derived from the component's own class output, not named in the test.\n\nTHREE CR-WEB-DEMO VALUES REPLACED BY FIGMA’S, NOW THAT THERE IS A FIGMA SOURCE. This pattern’s manifest recorded no `figma` node at all. (1) FILL: --color-surface-base, which is the PAGE colour — a grouping that grouped nothing visually. Figma draws color/gray/gray-850 #191f36. (2) BORDER: --color-border-neutral-soft, \"matches Card's resting border\". Figma draws color/gray/gray-700 #38425e. (3) RADIUS: 12px, chosen from cr-web-demo's \"three-step radius hierarchy … 12px for a panel nested inside one\". Figma draws radius/large 10px. That radius note is worth keeping as a shape: a careful, correct observation about a NON-FIGMA source, producing a wrong value because the source was not the authority.\n\nNEITHER RATIO IS A CONFLICT, and the original docblock's argument survives intact: the panel is 1.19:1 against the page and its border 1.63:1 against the panel, but a panel is a content grouping rather than a control, so 1.4.11's 3:1 does not apply to either. What the clause governs is the disclosure trigger, which keeps its own focus ring.",
  "figma": {
    "file": "zT3vGm4QlFEI2Jp9nxyFKI",
    "fileName": "Demo components library",
    "node": "22:5850",
    "componentSet": "Panel-configurations",
    "variants": [
      "State=Collapsed — 555px, gray-850 fill, gray-700 1px border, radius/large, spacing/small padding, plus glyph",
      "State=Expanded — same chrome, spacing/medium-small gap, minus glyph, four Dropdowns and two label-only rows"
    ],
    "note": "THIS PATTERN HAD NO FIGMA NODE UNTIL NOW. It was ported from cr-web-demo, so every value in it was a rule-2 breach rather than a port. Panel-configurations (22:5850) and Panel-profile (22:27807) both draw it and agree on the chrome, which is what makes this a binding rather than one screen’s evidence."
  }
}
