# DisclosureGroup

**Maturity:** experimental

A disclosure widget: a single tappable header (a label + a trailing chevron indicator) that toggles the visibility of arbitrary content beneath it.

## Import

```tsx
import { DisclosureGroup } from "@liquidify/react/disclosure-group"
```

## Props

- `animated`: `boolean | undefined`; default ``true``. When `false`, the expand/collapse roll (and its deferred unmount via {@link Presence}) is suppressed: toggling shows/hides `children` instantly, exactly as under `prefers-reduced-motion`. The chevron still rotates via CSS; kill that per-instance if an instant swap is wanted there too. Used by DatePicker, whose month/year wheel disclosure must not animate.
- `children`: `ReactNode`. The revealed content (SwiftUI `\@ViewBuilder content:`). Mounted in the DOM only while expanded (unmounted, not merely `hidden`, while collapsed) — supports a nested `<DisclosureGroup>`, which renders its own independent root with no auto-indent. Optional: a childless DisclosureGroup is a pure header toggle whose only effect is its event callbacks (e.g. lazy-loading content in {@link DisclosureGroupProps.onExpandedChange}).
- `className`: `string | undefined`. Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.
- `defaultExpanded`: `boolean | undefined`; default ``false``. Uncontrolled seed for the internal expanded state (ignored once {@link DisclosureGroupProps.expanded} is supplied). SwiftUI's own default is collapsed.
- `disabled`: `boolean | undefined`. When `true`, sets the native `disabled` attribute on the header `<button>`: removed from the tab order, not activatable via pointer or keyboard, dimmed via `--lq-opacity-muted`. The group's current collapsed/expanded content freezes at whatever it was.
- `expanded`: `boolean | undefined`. Controlled expanded state (SwiftUI `isExpanded: Binding<Bool>`). Passing this switches DisclosureGroup to controlled mode: the rendered state always reflects this value and the consumer owns it via {@link DisclosureGroupProps.onExpandedChange}.
- `icon`: `ReactNode`; default ``<Icon name="chevron-right" tint="accent" />``. The header's trailing glyph (SwiftUI disclosure indicator). Defaults to a right-pointing chevron `<Icon name="chevron-right" tint="accent" />` that rotates 90° on expand. To use a different glyph or colour, pass your OWN `<Icon>` (any `ReactNode`); it renders inside the rotating `.lq-disclosure-group-chevron` wrapper, so it still animates on toggle.
- `label`: `ReactNode` (required). The header's label (SwiftUI `titleKey:` / custom label view). A `string | number` routes through the shared `TextLabel` guard into an internal `Text` styled `variant="headline"`, `weight="bold"`, `color="primary"` — the fixed header typography (ADR-0028). To style it differently, pass your OWN `<Text>` (any `ReactNode`), which passes through unwrapped and owns its typography, OR use {@link DisclosureGroupProps.labelProps} to override individual axes while keeping the fixed structure. This is the button's accessible name.
- `labelProps`: `LabelProps | undefined`. Per-instance typography override for a `string | number` {@link * DisclosureGroupProps.label} (ADR-0028 rule 2, charter §2) — spread last into the header's primary `TextLabel`, so an explicit value here wins over the fixed `variant="headline"`/`weight="bold"`/`color="primary"` defaults. Ignored when `label` is already a `ReactNode` element (e.g. a caller-supplied `<Text>`), which passes through unwrapped and owns its own axes.
- `onExpandedChange`: `((expanded: boolean) => void) | undefined`. Fires with the next boolean on every toggle path — pointer click anywhere in the header row, or `Space` / `Enter` on the focused header button. Always called, in both controlled and uncontrolled modes. The sole semantic callback (charter §1 rule 5) — there is no public click-level prop.
- `ref`: `Ref<HTMLDivElement> | undefined`. Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.
- `variant`: `DisclosureGroupVariant | undefined`; default ``"plain"``. Material posture of the root.

## SwiftUI mapping

- ``DisclosureGroup(_ titleKey:, content:)` (string label)` → ``label: ReactNode`` (direct): Title/header. A `string`/`number` routes through the internal `Text` (`variant="headline"`, `weight="bold"`, `color="primary"`, ADR-0028); any other node passes through unwrapped. Becomes the header button's accessible name.
- ``init(content:, label:)` (custom label view)` → ``label: ReactNode`` (direct): Same prop covers the custom-label case — element labels pass through `TextLabel` unwrapped.
- ``content:` closure (`@ViewBuilder`, may be `EmptyView`)` → ``children?: ReactNode`` (direct): Arbitrary revealed content; supports a nested `<DisclosureGroup>` (no auto-indent — see Anatomy). **Optional** — a childless group is a pure header toggle whose effect is its callbacks; the expanded content region is simply empty.
- ``isExpanded: Binding<Bool>` (controlled)` → ``expanded?: boolean` + `onExpandedChange?: (expanded: boolean) => void`` (direct): The binding splits into the controlled-state prop + change callback — the presentation triad (ADR-0015), not the value-bearing `checked`/`onChange` shape (see Interaction). No public click-level prop exists (charter §1 rule 5): DisclosureGroup's header button is an internal interactive element, so `onExpandedChange(next)` is the sole semantic callback.
- `bare init (uncontrolled, self-managed)` → ``defaultExpanded?: boolean` (default `false`)` (renamed): Uncontrolled seed. SwiftUI's own default is collapsed.
- ``.disabled(_:)`` → ``disabled?: boolean`` (direct): Standard control state; not exercised in the demo, part of the full SwiftUI surface.
- ``.font(_:)` / `.foregroundStyle(_:)` on the label` → ``labelProps?: LabelProps`` (direct): Structured, token-backed typography override seam (ADR-0028) spread last into the header's primary `TextLabel`; a whole `<Text>` node passed as `label` bypasses it and owns its own axes.
- `disclosure-indicator glyph (SF Symbol `chevron.right`)` → ``icon?: ReactNode` (default `<Icon name="chevron-right" tint="accent" />`)` (direct): The header's trailing glyph. Renders inside the rotating `.lq-disclosure-group-chevron` wrapper, so a custom `<Icon>` still animates on expand. Pass your own node to change glyph or colour.
- ``.glassEffect(in: .rect(cornerRadius: 20))` wrapper (`"Nested on glass"`)` → ``variant?: "plain" | "translucent"` (default `"plain"`)` (direct): The glass panel applies ONLY to the translucent variant — mirrors `LabeledContent`'s `variant="translucent"` convention exactly.
- ``disclosureGroupStyle(_:)`` → `—` (direct): Not mapped for v1 — the demo exercises only the default `AutomaticDisclosureGroupStyle` (see Out of scope).
