# Collapsible

A single show/hide disclosure that expands and collapses a panel of content.

## Installation

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
```
## Usage

Pass `trigger` for the header label and `children` for the panel body. Pass a public `<Icon>` into
`icon` for the leading 16px glyph (Figma’s resting art includes it). The rotating chevron is on by
default (`indicator` defaults to `true`). Pass `trailing` for a header-end control (typically an
`IconButton`) — it renders as a sibling of the trigger, not inside it. Drive open state with
`defaultOpen` (uncontrolled) or `open` + `onOpenChange` (controlled). Pass `keepMounted` to leave
the panel in the DOM while closed, or `hiddenUntilFound` so closed content also stays findable via
the browser’s find-in-page — `hiddenUntilFound` alone already keeps the panel mounted, so the two
are not meant to be combined (Base UI Panel props).

Use `placement="sidebar"` for navigation sections: compact header insets, a rounded full-row hover
background, unpadded panel content, and an end-aligned chevron after the optional `trailing`
action. The action remains outside the disclosure button, so it does not toggle the section. The
default `placement="inline"` is unchanged. Pass `hug` on `inline` (or any placement) to drop panel
padding independently of `placement`.

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";

export default function BasicDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} tint="secondary" size="inherit" />}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```
## Anatomy

```tsx
import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible";
import {
  Collapsible,
  CollapsibleHeader,
  CollapsibleTrigger,
  CollapsibleTriggerTitle,
  CollapsibleTriggerTrailing,
  CollapsiblePanel,
  CollapsiblePanelContent,
} from "@makeplane/propel/components/collapsible";
// Styled Root frame (`w-full`) — separate from the ready-made `Collapsible` above (name collision).
import { Collapsible as CollapsibleFrame } from "@makeplane/propel/elements/collapsible";

// Ready-made — grafts Base UI Root → CollapsibleFrame for you
<Collapsible trigger="…" icon={…} trailing={…}>
  …
</Collapsible>

// Atomic parts — graft Base UI yourself
<BaseCollapsible.Root render={<CollapsibleFrame />}>
  <CollapsibleHeader>
    <BaseCollapsible.Trigger render={<CollapsibleTrigger />}>
      {icon}
      <CollapsibleTriggerTitle>…</CollapsibleTriggerTitle>
      {/* Rotating chevron — the shared caret is internal; supply your own */}
    </BaseCollapsible.Trigger>
    <CollapsibleTriggerTrailing>{/* IconButton, never nested in the trigger */}</CollapsibleTriggerTrailing>
  </CollapsibleHeader>
  <BaseCollapsible.Panel render={<CollapsiblePanel />}>
    <CollapsiblePanelContent>…</CollapsiblePanelContent>
  </BaseCollapsible.Panel>
</BaseCollapsible.Root>
```

The Root frame is full width so centered hosts don’t shrink-wrap when closed. The header is a flex
row: the trigger packs `[icon?][title][chevron]` as a left-aligned cluster and grows so leftover
space stays the toggle hit target; `CollapsibleTriggerTrailing` pins a sibling control at the
inline-end. Long titles wrap instead of staying single-line (Figma’s short sample uses nowrap).
Panel padding and body type live on `CollapsiblePanelContent` (`px-3 pb-3` for `inline`, `p-0` for
`sidebar` or whenever `hug` is set, + body-xs secondary) so the height animation on
`CollapsiblePanel` measures cleanly and List sections (children directly in the panel) do not
inherit Collapsible prose tokens. The rotating chevron
itself is an internal shared primitive (not exported) — an atomic assembly supplies its own, e.g. a
`ChevronDownOutline` from `@makeplane/propel/icons` rotated off `data-panel-open`.

## Examples

### Without icon

Omit `icon` when the row should be label + chevron only.

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";

export default function WithoutIconDemo() {
  return (
    <Collapsible trigger="Collapsible header">
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```

### Default open

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";

export default function DefaultOpenDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} tint="secondary" size="inherit" />}
      defaultOpen
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```

### Controlled

Pass `open` and `onOpenChange` to drive the disclosure from your own state instead of `defaultOpen`.

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";
import * as React from "react";

export default function ControlledDemo() {
  const [open, setOpen] = React.useState(false);
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} tint="secondary" size="inherit" />}
      open={open}
      onOpenChange={setOpen}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```

### Without indicator

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";

export default function WithoutIndicatorDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} tint="secondary" size="inherit" />}
      indicator={false}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```

### Trailing action

Pass `trailing` for a header-end control. It is a sibling of the trigger — never nest a button
inside the trigger. Clicking it does not toggle the panel. Root `disabled` does not disable this
control; disable the control itself if both should refuse input.

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { AddOutline } from "@makeplane/propel/icons";

export default function TrailingActionDemo() {
  return (
    <Collapsible
      trigger="Attachments 1"
      defaultOpen
      trailing={
        <IconButton
          variant="ghost"
          size="sm"
          aria-label="Add attachment"
          icon={<Icon icon={AddOutline} />}
        />
      }
    >
      Screen Recording 2026-08-28 at 7.20.21PM.mov
    </Collapsible>
  );
}
```

### Disabled

Pass Root `disabled` to dim the trigger and block open/close.

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";

export default function DisabledDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} tint="secondary" size="inherit" />}
      disabled
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```

### Keep mounted

Pass `keepMounted` to leave the panel in the DOM while closed. `hiddenUntilFound` does the same
(plus browser find-in-page) — pass one or the other, not both.

```tsx
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";

export default function KeepMountedDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} tint="secondary" size="inherit" />}
      keepMounted
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}
```
## Accessibility

- The trigger is a native `<button>` with Base UI's `aria-expanded` / `aria-controls`.
- The chevron is decorative (`aria-hidden`); the accessible name comes from the trigger label.
- A `trailing` control is a sibling of the trigger, never nested inside it (nested buttons are
  invalid). It is its own tab stop and does not toggle the panel.
- Disabled state dims the trigger and blocks open/close via Root `disabled` (`data-disabled` /
  `aria-disabled`) and swaps the cursor; the trigger stays focusable but will not toggle. Trailing
  controls are not disabled by Root `disabled`.
- Focus-visible draws the shared accent ring on the trigger.

## API Reference

### Collapsible

The ready-made collapsible: a single show/hide disclosure that wires the trigger and panel for
the 90% case. Pass `trigger` for the toggle button label and `children` for the body; optional
`icon` for a leading glyph and `trailing` for a header-end control (sibling of the trigger).
Forward `defaultOpen` (uncontrolled) or `open` + `onOpenChange` (controlled) to drive it. Set
`indicator={false}` to omit the rotating chevron from the trigger.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger` (required) | `ReactNode` | — | The button content that opens and closes the panel. |
| `children` | `ReactNode` | — | The collapsible content region. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | Element to graft the styled frame onto instead of the default `div` — element form only, the callback form's `state` isn't wired through the styled frame. |
| `icon` | `ReactNode` | — | Element rendered before the label (inline-start), e.g. `<Icon icon={Folder} tint="secondary" size="inherit" />`. |
| `trailing` | `ReactNode` | — | Control rendered at the header's inline-end, as a sibling of the trigger — never inside it (the trigger is a button; nested buttons are invalid). Typical content is an `IconButton`. Independent of open state: clicking it does not toggle the panel. Root `disabled` does not disable this control. |
| `placement` | `"inline" \| "sidebar"` | `inline` | Sidebar placement uses compact insets, an unpadded body, and an independent action before the end-aligned indicator. Inline placement keeps the label and indicator together. |
| `indicator` | `boolean` | `true` | Whether to show the rotating chevron indicator after the trigger label. |
| `keepMounted` | `boolean` | — | Keep the panel in the DOM while closed — Base UI's `Panel.keepMounted`. |
| `hiddenUntilFound` | `boolean` | — | Reveal on the browser's find-in-page — Base UI's `Panel.hiddenUntilFound`. |
| `hug` | `boolean` | `false` | Drop the content region's own padding, for content that brings its own spacing. Independent of `placement` — `placement="sidebar"` already defaults to unpadded content; `hug` lets `inline` (or a future placement) opt into the same look without switching placement. |
