diff --git a/.changeset/add-disclosure-component.md b/.changeset/add-disclosure-component.md
new file mode 100644
index 000000000..7641d611d
--- /dev/null
+++ b/.changeset/add-disclosure-component.md
@@ -0,0 +1,14 @@
+---
+'@cube-dev/ui-kit': minor
+---
+
+Add new `Disclosure` component for expandable/collapsible content sections. Features include:
+
+- `Disclosure` - Single expandable panel with trigger and content
+- `Disclosure.Trigger` - Built on ItemButton with full support for icons, descriptions, and actions
+- `Disclosure.Content` - Collapsible content area with smooth height animations
+- `Disclosure.Group` - Accordion container for multiple disclosures with single or multiple expanded support
+- `Disclosure.Item` - Individual item within a group
+
+Supports controlled/uncontrolled state, `shape` variants (`default`, `card`, `sharp`), disabled state, custom transition duration, and render prop API for custom triggers.
+
diff --git a/src/components/content/Disclosure/Disclosure.docs.mdx b/src/components/content/Disclosure/Disclosure.docs.mdx
new file mode 100644
index 000000000..38e326c2f
--- /dev/null
+++ b/src/components/content/Disclosure/Disclosure.docs.mdx
@@ -0,0 +1,209 @@
+import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';
+import { Disclosure } from './Disclosure';
+import * as DisclosureStories from './Disclosure.stories';
+
+
+
+# Disclosure
+
+An accessible collapsible container that reveals or hides additional content. Built on React Aria's `useDisclosure` hook, it provides keyboard navigation, screen reader support, and smooth height animations. Supports both standalone usage and grouped accordion behavior.
+
+## When to Use
+
+- For FAQ sections where users can expand answers to questions
+- To hide supplementary information that shouldn't overwhelm the main content
+- For accordion-style navigation or settings panels
+- When you need collapsible sections with consistent accessibility patterns
+- To progressively disclose complex information
+
+## Component
+
+
+
+---
+
+### Properties
+
+
+
+### Base Properties
+
+Supports [Base properties](/BaseProperties)
+
+## Compound Components
+
+### Disclosure.Trigger
+
+The clickable button that toggles the disclosure. Built on `ItemButton` and accepts all ItemButton props including `icon`, `type`, `theme`, `size`, and more.
+
+### Disclosure.Content
+
+The collapsible panel containing the hidden content. Animates smoothly using CSS `interpolate-size: allow-keywords` for height transitions.
+
+### Disclosure.Group
+
+Container for multiple disclosure items that coordinates their expanded states. By default, only one item can be expanded at a time (accordion behavior).
+
+
+
+### Disclosure.Item
+
+Individual disclosure within a group. Must have a unique `id` prop when used inside `Disclosure.Group`.
+
+## Styling
+
+### styles
+
+Customizes the root element of the Disclosure component.
+
+### triggerStyles
+
+When using `Disclosure.Trigger`, you can pass `styles` prop to customize the trigger button.
+
+### contentStyles
+
+When using `Disclosure.Content`, you can pass `styles` prop to customize the content panel.
+
+### Group-level Styling
+
+`Disclosure.Group` accepts `triggerProps` to apply consistent props to all triggers, and `contentStyles` to style all content panels uniformly.
+
+### Style Properties
+
+Direct style application without using the `styles` prop: `width`, `height`, `padding`, `margin`, `gap`.
+
+### Modifiers
+
+The `mods` property accepts the following modifiers:
+
+| Modifier | Type | Description |
+|----------|------|-------------|
+| `expanded` | `boolean` | True when the disclosure content is visible |
+| `disabled` | `boolean` | True when interactions are disabled |
+| `shape` | `string` | The current shape variant (`default`, `card`, `sharp`) |
+
+For `Disclosure.Content`, additional modifiers are available:
+
+| Modifier | Type | Description |
+|----------|------|-------------|
+| `shown` | `boolean` | True when content is visible (controls height animation) |
+| `phase` | `string` | Transition phase (`enter`, `entered`, `exit`, `unmounted`) |
+
+## Variants
+
+### Shapes
+
+#### Card
+
+Bordered container with rounded corners.
+
+
+
+#### Sharp
+
+Sharp edges with no border radius.
+
+
+
+## Examples
+
+### Default Expanded
+
+Disclosure that starts in expanded state using `defaultExpanded` prop.
+
+
+
+### Disabled
+
+Disabled disclosure that cannot be toggled.
+
+
+
+### Controlled State
+
+Control the disclosure state externally with `isExpanded` and `onExpandedChange`.
+
+
+
+### Multiple Expanded Items
+
+Group with `allowsMultipleExpanded` where multiple items can be open simultaneously.
+
+
+
+### Default Expanded Keys
+
+Group with specific items pre-expanded via `defaultExpandedKeys`.
+
+
+
+### Custom Trigger with Render Prop
+
+Use render prop pattern to create custom triggers with access to `isExpanded` and `toggle`.
+
+
+
+### Disabled Group
+
+Group-level `isDisabled` that disables all items at once.
+
+
+
+### Nested Disclosures
+
+Disclosures can be nested, each maintaining independent state.
+
+
+
+## Accessibility
+
+### Keyboard Navigation
+
+- `Tab` - Moves focus to the disclosure trigger
+- `Space` / `Enter` - Toggles the disclosure open/closed
+- `Tab` (when expanded) - Moves focus into the content panel
+
+### Screen Reader Support
+
+- Trigger announces as a button with expanded/collapsed state
+- `aria-expanded` is automatically managed by React Aria
+- `aria-controls` links trigger to content panel automatically
+- State changes are announced when toggling
+
+### ARIA Properties
+
+- `aria-expanded` - Indicates whether content is visible (managed automatically)
+- `aria-controls` - Links trigger to panel (managed automatically)
+- `aria-disabled` - Applied when `isDisabled` is true
+
+## Best Practices
+
+1. **Do**: Provide clear, descriptive trigger labels
+
+ ```jsx
+ View shipping details
+ ```
+
+2. **Don't**: Use vague or icon-only triggers without labels
+
+ ```jsx
+ +
+ ```
+
+3. **Do**: Use groups for related collapsible sections
+
+ ```jsx
+
+ ...
+ ...
+
+ ```
+
+4. **Accessibility**: Always ensure trigger text clearly indicates what will be revealed
+
+5. **Performance**: Content remains mounted during transitions for smooth animations and accessibility
+
+## Related Components
+
+- [ItemButton](/docs/actions-itembutton--docs) - The base component used for triggers
+- [DisplayTransition](/docs/helpers-displaytransition--docs) - Manages animation phases
diff --git a/src/components/content/Disclosure/Disclosure.stories.tsx b/src/components/content/Disclosure/Disclosure.stories.tsx
new file mode 100644
index 000000000..044f7dfc5
--- /dev/null
+++ b/src/components/content/Disclosure/Disclosure.stories.tsx
@@ -0,0 +1,421 @@
+import { useState } from 'react';
+
+import { Button } from '../../actions/Button';
+import { Switch } from '../../fields/Switch';
+import { Space } from '../../layout/Space';
+import { Divider } from '../Divider';
+import { Paragraph } from '../Paragraph';
+
+import { Disclosure } from './Disclosure';
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+const meta = {
+ title: 'Content/Disclosure',
+ component: Disclosure,
+ parameters: {
+ layout: 'padded',
+ },
+ argTypes: {
+ /* State Control */
+ isExpanded: {
+ control: 'boolean',
+ description: 'Controls expanded state in controlled mode',
+ table: {
+ type: { summary: 'boolean' },
+ },
+ },
+ defaultExpanded: {
+ control: 'boolean',
+ description: 'Initial expanded state in uncontrolled mode',
+ table: {
+ type: { summary: 'boolean' },
+ defaultValue: { summary: false },
+ },
+ },
+ onExpandedChange: {
+ action: 'expanded-change',
+ description: 'Callback fired when expanded state changes',
+ table: {
+ type: { summary: '(isExpanded: boolean) => void' },
+ },
+ },
+
+ /* Behavior */
+ isDisabled: {
+ control: 'boolean',
+ description: 'Disables trigger interactions and force-closes the content',
+ table: {
+ type: { summary: 'boolean' },
+ defaultValue: { summary: false },
+ },
+ },
+
+ /* Appearance */
+ shape: {
+ control: 'radio',
+ options: ['default', 'card', 'sharp'],
+ description:
+ 'Visual shape variant: default (no styling), card (border/radius), sharp (radius 0)',
+ table: {
+ type: { summary: "'default' | 'card' | 'sharp'" },
+ defaultValue: { summary: 'default' },
+ },
+ },
+
+ /* Animation */
+ transitionDuration: {
+ control: 'number',
+ description:
+ 'Duration for DisplayTransition animation in milliseconds. When undefined, uses default CSS transition timing',
+ table: {
+ type: { summary: 'number' },
+ },
+ },
+
+ /* Content */
+ children: {
+ control: { type: null },
+ description: 'Disclosure.Trigger and Disclosure.Content components',
+ table: {
+ type: {
+ summary: 'ReactNode | ((state: DisclosureStateContext) => ReactNode)',
+ },
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+/**
+ * Basic single disclosure with trigger and content
+ */
+export const SingleDisclosure: Story = {
+ render: (args) => (
+
+ What is Cube?
+
+
+ Cube is a semantic layer that connects data sources to applications.
+ It provides a unified interface for data modeling, access control, and
+ caching.
+
+
+
+ ),
+};
+
+/**
+ * Multiple disclosures in a group with single-open (accordion) behavior
+ */
+export const GroupDisclosure: Story = {
+ render: () => (
+
+
+ What is Cube?
+
+
+ Cube is a semantic layer that connects data sources to applications.
+ It provides a unified interface for data modeling, access control,
+ and caching.
+
+
+
+
+
+ How does it work?
+
+
+ Cube connects to your data sources, allows you to define metrics and
+ dimensions, and serves them through a unified API. It handles query
+ optimization and caching automatically.
+
+
+
+
+
+ What are the benefits?
+
+
+ Cube provides consistency across your data stack, improves
+ performance with intelligent caching, ensures governance with
+ centralized access control, and scales efficiently with your data
+ needs.
+
+
+
+
+ ),
+};
+
+/**
+ * Card shape variant with border and rounded corners
+ */
+export const CardShape: Story = {
+ render: (args) => (
+
+ What is Cube?
+
+
+ Cube is a semantic layer that connects data sources to applications.
+ It provides a unified interface for data modeling, access control, and
+ caching.
+
+
+
+ ),
+};
+
+/**
+ * Sharp shape variant with no border radius
+ */
+export const SharpShape: Story = {
+ render: (args) => (
+
+ What is Cube?
+
+
+ Cube is a semantic layer that connects data sources to applications.
+ It provides a unified interface for data modeling, access control, and
+ caching.
+
+
+
+ ),
+};
+
+/**
+ * Disclosure that starts in expanded state
+ */
+export const DefaultExpanded: Story = {
+ render: (args) => (
+
+ Already expanded
+
+
+ This content is visible by default when the component mounts.
+
+
+
+ ),
+};
+
+/**
+ * Disabled disclosure that cannot be toggled
+ */
+export const Disabled: Story = {
+ render: (args) => (
+
+ Cannot be toggled
+
+ This content will never be visible.
+
+
+ ),
+};
+
+/**
+ * Controlled disclosure with external state management
+ */
+export const Controlled: Story = {
+ render: function ControlledStory(args) {
+ const [isExpanded, setExpanded] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+ Controlled disclosure
+
+
+ This disclosure is controlled by external buttons.
+
+
+
+
+ );
+ },
+};
+
+/**
+ * Group allowing multiple items to be expanded simultaneously
+ */
+export const MultipleExpanded: Story = {
+ render: () => (
+
+
+ First item
+
+
+ This can be open at the same time as other items.
+
+
+
+
+
+ Second item
+
+ Multiple items can be expanded simultaneously.
+
+
+
+
+ Third item
+
+
+ Unlike accordion mode, opening one doesn't close others.
+
+
+
+
+ ),
+};
+
+/**
+ * Group with some items pre-expanded
+ */
+export const DefaultExpandedKeys: Story = {
+ render: () => (
+
+
+ First item (collapsed)
+
+ First item content.
+
+
+
+
+
+ Second item (expanded by default)
+
+
+
+ This item starts expanded via defaultExpandedKeys.
+
+
+
+
+
+ Third item (collapsed)
+
+ Third item content.
+
+
+
+ ),
+};
+
+/**
+ * Using render prop for custom trigger structure
+ */
+export const RenderProp: Story = {
+ render: (args) => (
+
+ {({ isExpanded, toggle }) => (
+ <>
+
+
+
+ Custom trigger using render prop pattern. The trigger can be any
+ component that calls the toggle function.
+
+
+ >
+ )}
+
+ ),
+};
+
+/**
+ * Disabled group where all items are non-interactive
+ */
+export const DisabledGroup: Story = {
+ render: () => (
+
+
+ Cannot toggle
+
+ Hidden content.
+
+
+
+
+ Also disabled
+
+ Hidden content.
+
+
+
+ ),
+};
+
+/**
+ * Nested disclosures
+ */
+export const Nested: Story = {
+ render: (args) => (
+
+ Outer disclosure
+
+ Outer content with a nested disclosure:
+
+
+ Inner disclosure
+
+
+ Nested disclosure content. Each disclosure maintains independent
+ state.
+
+
+
+
+
+ ),
+};
+
+/**
+ * Trigger with actions containing a Switch to control disabled state
+ */
+export const TriggerWithActions: Story = {
+ render: function TriggerWithActionsStory(args) {
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ return (
+
+ setIsDisabled(!value)}
+ />
+ }
+ >
+ Toggle disabled state with the switch
+
+
+
+ Use the switch in the trigger to disable/enable this disclosure.
+ When disabled, the content cannot be toggled.
+
+
+
+ );
+ },
+};
diff --git a/src/components/content/Disclosure/Disclosure.test.tsx b/src/components/content/Disclosure/Disclosure.test.tsx
new file mode 100644
index 000000000..4daf58c59
--- /dev/null
+++ b/src/components/content/Disclosure/Disclosure.test.tsx
@@ -0,0 +1,489 @@
+import { act, renderWithRoot, userEvent, waitFor } from '../../../test';
+
+import { Disclosure } from './Disclosure';
+
+jest.mock('../../../_internal/hooks/use-warn');
+
+describe('', () => {
+ describe('Basic Rendering', () => {
+ it('should render trigger and content', () => {
+ const { getByRole, getByText } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button', { name: 'Toggle' })).toBeInTheDocument();
+ expect(getByText('Content')).toBeInTheDocument();
+ });
+
+ it('should add data-qa attribute', () => {
+ const { getByTestId } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByTestId('test-disclosure')).toBeInTheDocument();
+ });
+
+ it('should add data-qa to content', () => {
+ const { container } = renderWithRoot(
+
+ Toggle
+
+ Content
+
+ ,
+ );
+
+ expect(
+ container.querySelector('[data-qa="disclosure-content"]'),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Expand/Collapse', () => {
+ it('should start collapsed by default', () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button')).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('should expand on click', async () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ const trigger = getByRole('button');
+
+ await userEvent.click(trigger);
+
+ await waitFor(() => {
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ });
+ });
+
+ it('should collapse on second click', async () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ const trigger = getByRole('button');
+
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+
+ await userEvent.click(trigger);
+
+ await waitFor(() => {
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
+ });
+ });
+
+ it('should call onExpandedChange when expanded', async () => {
+ const onExpandedChange = jest.fn();
+
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ await userEvent.click(getByRole('button'));
+
+ await waitFor(() => {
+ expect(onExpandedChange).toHaveBeenCalledWith(true);
+ });
+ });
+ });
+
+ describe('Keyboard Interactions', () => {
+ it('should expand on Enter key', async () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ const trigger = getByRole('button');
+
+ await act(async () => {
+ trigger.focus();
+ });
+
+ await userEvent.keyboard('{Enter}');
+
+ await waitFor(() => {
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ });
+ });
+
+ it('should expand on Space key', async () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ const trigger = getByRole('button');
+
+ await act(async () => {
+ trigger.focus();
+ });
+
+ await userEvent.keyboard(' ');
+
+ await waitFor(() => {
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ });
+ });
+ });
+
+ describe('Controlled State', () => {
+ it('should respect controlled isExpanded prop', () => {
+ const { getByRole, rerender } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button')).toHaveAttribute('aria-expanded', 'false');
+
+ rerender(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button')).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('should respect defaultExpanded prop', () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button')).toHaveAttribute('aria-expanded', 'true');
+ });
+ });
+
+ describe('Disabled State', () => {
+ it('should disable trigger when isDisabled is true', () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button')).toHaveAttribute('aria-disabled', 'true');
+ });
+
+ it('should not expand when disabled', async () => {
+ const onExpandedChange = jest.fn();
+
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ await userEvent.click(getByRole('button'));
+
+ expect(onExpandedChange).not.toHaveBeenCalled();
+ expect(getByRole('button')).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('should force close when disabled even if defaultExpanded', () => {
+ const { getByRole } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(getByRole('button')).toHaveAttribute('aria-expanded', 'false');
+ });
+ });
+
+ describe('ARIA Attributes', () => {
+ it('should have correct aria-controls linking', () => {
+ const { getByRole, container } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ const trigger = getByRole('button');
+ const ariaControls = trigger.getAttribute('aria-controls');
+
+ expect(ariaControls).toBeTruthy();
+ expect(container.querySelector(`#${ariaControls}`)).toBeInTheDocument();
+ });
+ });
+
+ describe('Render Prop', () => {
+ it('should provide state context via render prop', async () => {
+ const { getByRole, getByText } = renderWithRoot(
+
+ {({ isExpanded, toggle }) => (
+ <>
+
+ {isExpanded ? 'Open' : 'Closed'}
+ >
+ )}
+ ,
+ );
+
+ expect(getByText('Closed')).toBeInTheDocument();
+
+ await userEvent.click(getByRole('button'));
+
+ await waitFor(() => {
+ expect(getByText('Open')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Shape Variants', () => {
+ it('should render with card shape', () => {
+ const { container } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(
+ container.querySelector('[data-shape="card"]'),
+ ).toBeInTheDocument();
+ });
+
+ it('should render with sharp shape', () => {
+ const { container } = renderWithRoot(
+
+ Toggle
+ Content
+ ,
+ );
+
+ expect(
+ container.querySelector('[data-shape="sharp"]'),
+ ).toBeInTheDocument();
+ });
+ });
+});
+
+describe('', () => {
+ it('should render multiple disclosure items', () => {
+ const { getAllByRole } = renderWithRoot(
+
+
+ Item 1
+ Content 1
+
+
+ Item 2
+ Content 2
+
+ ,
+ );
+
+ expect(getAllByRole('button')).toHaveLength(2);
+ });
+
+ it('should only allow single expanded by default (accordion)', async () => {
+ const { getAllByRole } = renderWithRoot(
+
+
+ Item 1
+ Content 1
+
+
+ Item 2
+ Content 2
+
+ ,
+ );
+
+ const [trigger1, trigger2] = getAllByRole('button');
+
+ // Expand first item
+ await userEvent.click(trigger1);
+
+ await waitFor(() => {
+ expect(trigger1).toHaveAttribute('aria-expanded', 'true');
+ });
+ expect(trigger2).toHaveAttribute('aria-expanded', 'false');
+
+ // Expand second item - should collapse first
+ await userEvent.click(trigger2);
+
+ await waitFor(() => {
+ expect(trigger2).toHaveAttribute('aria-expanded', 'true');
+ });
+ expect(trigger1).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('should allow multiple expanded when allowsMultipleExpanded is true', async () => {
+ const { getAllByRole } = renderWithRoot(
+
+
+ Item 1
+ Content 1
+
+
+ Item 2
+ Content 2
+
+ ,
+ );
+
+ const [trigger1, trigger2] = getAllByRole('button');
+
+ // Expand first item
+ await userEvent.click(trigger1);
+
+ await waitFor(() => {
+ expect(trigger1).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ // Expand second item
+ await userEvent.click(trigger2);
+
+ await waitFor(() => {
+ expect(trigger2).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ // Both should be expanded
+ expect(trigger1).toHaveAttribute('aria-expanded', 'true');
+ expect(trigger2).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('should respect defaultExpandedKeys', () => {
+ const { getAllByRole } = renderWithRoot(
+
+
+ Item 1
+ Content 1
+
+
+ Item 2
+ Content 2
+
+ ,
+ );
+
+ const [trigger1, trigger2] = getAllByRole('button');
+
+ expect(trigger1).toHaveAttribute('aria-expanded', 'false');
+ expect(trigger2).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('should call onExpandedChange with updated keys', async () => {
+ const onExpandedChange = jest.fn();
+
+ const { getAllByRole } = renderWithRoot(
+
+
+ Item 1
+ Content 1
+
+
+ Item 2
+ Content 2
+
+ ,
+ );
+
+ const [trigger1] = getAllByRole('button');
+
+ await userEvent.click(trigger1);
+
+ await waitFor(() => {
+ expect(onExpandedChange).toHaveBeenCalledWith(new Set(['1']));
+ });
+ });
+
+ it('should disable all items when group isDisabled', () => {
+ const { getAllByRole } = renderWithRoot(
+
+
+ Item 1
+ Content 1
+
+
+ Item 2
+ Content 2
+
+ ,
+ );
+
+ const triggers = getAllByRole('button');
+
+ triggers.forEach((trigger) => {
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
+ });
+ });
+});
+
+describe('Nested Disclosures', () => {
+ it('should maintain independent state for nested disclosures', async () => {
+ const { getAllByRole } = renderWithRoot(
+
+ Outer
+
+
+ Inner
+ Inner Content
+
+
+ ,
+ );
+
+ const [outerTrigger, innerTrigger] = getAllByRole('button');
+
+ expect(outerTrigger).toHaveAttribute('aria-expanded', 'true');
+ expect(innerTrigger).toHaveAttribute('aria-expanded', 'false');
+
+ // Expand inner
+ await userEvent.click(innerTrigger);
+
+ await waitFor(() => {
+ expect(innerTrigger).toHaveAttribute('aria-expanded', 'true');
+ });
+ expect(outerTrigger).toHaveAttribute('aria-expanded', 'true');
+
+ // Collapse outer - inner state should be independent
+ await userEvent.click(outerTrigger);
+
+ await waitFor(() => {
+ expect(outerTrigger).toHaveAttribute('aria-expanded', 'false');
+ });
+ // Inner keeps its state even though outer is collapsed
+ expect(innerTrigger).toHaveAttribute('aria-expanded', 'true');
+ });
+});
diff --git a/src/components/content/Disclosure/Disclosure.tsx b/src/components/content/Disclosure/Disclosure.tsx
new file mode 100644
index 000000000..c001940fe
--- /dev/null
+++ b/src/components/content/Disclosure/Disclosure.tsx
@@ -0,0 +1,632 @@
+import {
+ createContext,
+ forwardRef,
+ Key,
+ ReactNode,
+ RefObject,
+ useContext,
+ useMemo,
+ useRef,
+} from 'react';
+import { mergeProps, useDisclosure, useId } from 'react-aria';
+import {
+ DisclosureGroupState,
+ DisclosureState,
+ useDisclosureGroupState,
+ useDisclosureState,
+} from 'react-stately';
+
+import { RightIcon } from '../../../icons';
+import {
+ BaseProps,
+ BasePropsWithoutChildren,
+ extractStyles,
+ OUTER_STYLES,
+ OuterStyleProps,
+ Styles,
+ tasty,
+} from '../../../tasty';
+import { mergeRefs } from '../../../utils/react';
+import { CubeItemButtonProps, ItemButton } from '../../actions/ItemButton';
+import { DisplayTransition } from '../../helpers';
+
+// ============================================================================
+// Types
+// ============================================================================
+
+export interface DisclosureStateContext {
+ isExpanded: boolean;
+ toggle: () => void;
+ expand: () => void;
+ collapse: () => void;
+}
+
+interface DisclosureContextValue {
+ state: DisclosureState;
+ buttonProps: Record;
+ panelProps: Record;
+ panelRef: RefObject;
+ isDisabled: boolean;
+ isExpanded: boolean;
+ shape: 'default' | 'card' | 'sharp';
+ transitionDuration?: number;
+ triggerProps?: Partial;
+ contentStyles?: Styles;
+}
+
+interface DisclosureGroupContextValue {
+ groupState: DisclosureGroupState;
+ triggerProps?: Partial;
+ contentStyles?: Styles;
+}
+
+export interface CubeDisclosureProps
+ extends BasePropsWithoutChildren,
+ OuterStyleProps {
+ /** Controls expanded state in controlled mode */
+ isExpanded?: boolean;
+ /** Initial expanded state in uncontrolled mode */
+ defaultExpanded?: boolean;
+ /** Callback fired when expanded state changes */
+ onExpandedChange?: (isExpanded: boolean) => void;
+ /** Disables trigger interactions and force-closes the content */
+ isDisabled?: boolean;
+ /** Render-prop alternative to achieve custom trigger markup */
+ children?: ReactNode | ((state: DisclosureStateContext) => ReactNode);
+ /** Visual shape variant */
+ shape?: 'default' | 'card' | 'sharp';
+ /** Duration for DisplayTransition animation in milliseconds */
+ transitionDuration?: number;
+}
+
+export interface CubeDisclosureTriggerProps
+ extends Omit {
+ /** Children content for the trigger */
+ children?: ReactNode;
+}
+
+export interface CubeDisclosureContentProps extends BaseProps {
+ /** Children content for the panel */
+ children?: ReactNode;
+}
+
+export interface CubeDisclosureGroupProps extends BaseProps, OuterStyleProps {
+ /** Allow more than one disclosure to be open */
+ allowsMultipleExpanded?: boolean;
+ /** Controlled expanded keys */
+ expandedKeys?: Iterable;
+ /** Uncontrolled default expanded keys */
+ defaultExpandedKeys?: Iterable;
+ /** Change handler providing the full expanded keys Set */
+ onExpandedChange?: (keys: Set) => void;
+ /** Disable all disclosures within group and force-close their content */
+ isDisabled?: boolean;
+ /** Props forwarded to all ItemButton triggers in the group */
+ triggerProps?: Partial;
+ /** Optional panel styles applied to all Content panels in the group */
+ contentStyles?: Styles;
+ children: ReactNode;
+}
+
+export interface CubeDisclosureItemProps
+ extends Omit,
+ OuterStyleProps {
+ /** Unique identifier for the disclosure item in a group */
+ id?: Key;
+ /** Children content */
+ children?: ReactNode;
+ /** Disables trigger interactions and force-closes the content */
+ isDisabled?: boolean;
+ /** Controls expanded state in controlled mode */
+ isExpanded?: boolean;
+ /** Initial expanded state in uncontrolled mode */
+ defaultExpanded?: boolean;
+ /** Callback fired when expanded state changes */
+ onExpandedChange?: (isExpanded: boolean) => void;
+ /** Visual shape variant */
+ shape?: 'default' | 'card' | 'sharp';
+}
+
+// ============================================================================
+// Contexts
+// ============================================================================
+
+const DisclosureContext = createContext(null);
+const DisclosureGroupContext =
+ createContext(null);
+
+function useDisclosureContext(): DisclosureContextValue {
+ const context = useContext(DisclosureContext);
+
+ if (!context) {
+ throw new Error(
+ 'Disclosure.Trigger and Disclosure.Content must be used within a Disclosure',
+ );
+ }
+
+ return context;
+}
+
+function useDisclosureGroupContext(): DisclosureGroupContextValue | null {
+ return useContext(DisclosureGroupContext);
+}
+
+// ============================================================================
+// Styled Components
+// ============================================================================
+
+const DisclosureRoot = tasty({
+ qa: 'Disclosure',
+ styles: {
+ display: 'flex',
+ flow: 'column',
+ gap: 0,
+ position: 'relative',
+ border: {
+ '': 'none',
+ 'shape=card': '1bw solid #border',
+ },
+ radius: {
+ '': '1r',
+ 'shape=card': '1cr',
+ 'shape=sharp': '0',
+ },
+ fill: '#white',
+ },
+});
+
+const ContentWrapperElement = tasty({
+ styles: {
+ display: 'block',
+ overflow: 'hidden',
+ interpolateSize: 'allow-keywords',
+ height: {
+ '': '0',
+ shown: 'max-content',
+ },
+ transition: 'height $disclosure-transition linear',
+ },
+});
+
+const ContentElement = tasty({
+ qa: 'DisclosureContent',
+ styles: {
+ padding: '1x',
+ },
+});
+
+const GroupRoot = tasty({
+ qa: 'DisclosureGroup',
+ styles: {
+ display: 'flex',
+ flow: 'column',
+ gap: '0',
+ },
+});
+
+const TriggerIcon = tasty(RightIcon, {
+ styles: {
+ transition: 'rotate',
+ rotate: {
+ '': '0deg',
+ expanded: '90deg',
+ },
+ },
+});
+
+const StyledTrigger = tasty(ItemButton, {
+ styles: {
+ radius: {
+ '': '1r',
+ 'expanded & shape=card': '(1cr - 1bw) (1cr - 1bw) 0 0',
+ 'shape=sharp': '0',
+ },
+ border: '#clear',
+ },
+});
+
+// ============================================================================
+// Disclosure Component
+// ============================================================================
+
+const DisclosureComponent = forwardRef(
+ function Disclosure(props, ref) {
+ const {
+ isExpanded: controlledIsExpanded,
+ defaultExpanded,
+ onExpandedChange,
+ isDisabled = false,
+ children,
+ shape = 'default',
+ transitionDuration,
+ qa,
+ mods,
+ ...otherProps
+ } = props;
+
+ const groupContext = useDisclosureGroupContext();
+ const panelRef = useRef(null);
+
+ // When disabled, force expanded to false
+ const effectiveIsExpanded = isDisabled ? false : controlledIsExpanded;
+
+ const state = useDisclosureState({
+ isExpanded: effectiveIsExpanded,
+ defaultExpanded: isDisabled ? false : defaultExpanded,
+ onExpandedChange: isDisabled ? undefined : onExpandedChange,
+ });
+
+ // When disabled, override state.isExpanded to false
+ const isExpanded = isDisabled ? false : state.isExpanded;
+
+ const { buttonProps, panelProps } = useDisclosure(
+ {
+ isExpanded,
+ isDisabled,
+ },
+ state,
+ panelRef,
+ );
+
+ const contextValue = useMemo(
+ () => ({
+ state,
+ buttonProps,
+ panelProps,
+ panelRef,
+ isDisabled,
+ isExpanded,
+ shape,
+ transitionDuration,
+ triggerProps: groupContext?.triggerProps,
+ contentStyles: groupContext?.contentStyles,
+ }),
+ [
+ state,
+ buttonProps,
+ panelProps,
+ isDisabled,
+ isExpanded,
+ shape,
+ transitionDuration,
+ groupContext?.triggerProps,
+ groupContext?.contentStyles,
+ ],
+ );
+
+ const stateContext = useMemo(
+ () => ({
+ isExpanded,
+ toggle: state.toggle,
+ expand: state.expand,
+ collapse: state.collapse,
+ }),
+ [isExpanded, state.toggle, state.expand, state.collapse],
+ );
+
+ const outerStyles = extractStyles(otherProps, OUTER_STYLES);
+
+ const finalMods = useMemo(
+ () => ({
+ expanded: isExpanded,
+ disabled: isDisabled,
+ shape,
+ ...mods,
+ }),
+ [isExpanded, isDisabled, shape, mods],
+ );
+
+ const content =
+ typeof children === 'function' ? children(stateContext) : children;
+
+ return (
+
+
+ {content}
+
+
+ );
+ },
+);
+
+// ============================================================================
+// Disclosure.Trigger Component
+// ============================================================================
+
+const DisclosureTrigger = forwardRef<
+ HTMLButtonElement,
+ CubeDisclosureTriggerProps
+>(function DisclosureTrigger(props, ref) {
+ const { children, icon, mods, ...otherProps } = props;
+ const context = useDisclosureContext();
+ const { buttonProps, isDisabled, isExpanded, shape, triggerProps } = context;
+
+ const finalMods = useMemo(
+ () => ({
+ expanded: isExpanded,
+ disabled: isDisabled,
+ shape,
+ ...mods,
+ }),
+ [isExpanded, isDisabled, shape, mods],
+ );
+
+ // Default icon is a rotating chevron
+ const defaultIcon = ;
+
+ return (
+
+ {children}
+
+ );
+});
+
+// ============================================================================
+// Disclosure.Content Component
+// ============================================================================
+
+const DisclosureContent = forwardRef<
+ HTMLDivElement,
+ CubeDisclosureContentProps
+>(function DisclosureContent(props, ref) {
+ const { children, styles, mods, ...otherProps } = props;
+ const context = useDisclosureContext();
+ const {
+ panelProps,
+ panelRef,
+ isExpanded,
+ transitionDuration,
+ contentStyles,
+ } = context;
+
+ const mergedStyles = useMemo(
+ () => ({
+ ...contentStyles,
+ ...styles,
+ }),
+ [contentStyles, styles],
+ );
+
+ // Filter out hidden attribute from panelProps since we manage visibility via CSS height animation
+ const { hidden, ...filteredPanelProps } = panelProps as Record<
+ string,
+ unknown
+ >;
+
+ return (
+
+ {({ phase, isShown, ref: transitionRef }) => (
+
+
+ {children}
+
+
+ )}
+
+ );
+});
+
+// ============================================================================
+// Disclosure.Group Component
+// ============================================================================
+
+const DisclosureGroup = forwardRef(
+ function DisclosureGroup(props, ref) {
+ const {
+ allowsMultipleExpanded = false,
+ expandedKeys,
+ defaultExpandedKeys,
+ onExpandedChange,
+ isDisabled = false,
+ triggerProps,
+ contentStyles,
+ children,
+ qa,
+ mods,
+ styles,
+ ...otherProps
+ } = props;
+
+ const groupState = useDisclosureGroupState({
+ allowsMultipleExpanded,
+ expandedKeys,
+ defaultExpandedKeys,
+ onExpandedChange,
+ isDisabled,
+ });
+
+ const contextValue = useMemo(
+ () => ({
+ groupState,
+ triggerProps,
+ contentStyles,
+ }),
+ [groupState, triggerProps, contentStyles],
+ );
+
+ const outerStyles = extractStyles(otherProps, OUTER_STYLES);
+
+ const finalStyles = useMemo(
+ () => ({
+ ...outerStyles,
+ ...styles,
+ }),
+ [outerStyles, styles],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+// ============================================================================
+// Disclosure.Item Component
+// ============================================================================
+
+const DisclosureItem = forwardRef(
+ function DisclosureItem(props, ref) {
+ const {
+ id: providedId,
+ children,
+ isDisabled: itemDisabled = false,
+ isExpanded: controlledIsExpanded,
+ defaultExpanded,
+ onExpandedChange,
+ shape = 'default',
+ qa,
+ mods,
+ styles,
+ ...otherProps
+ } = props;
+
+ const defaultId = useId();
+ const id = providedId ?? defaultId;
+
+ const groupContext = useDisclosureGroupContext();
+ const panelRef = useRef(null);
+
+ // Determine if disabled from group or item
+ const isDisabled =
+ itemDisabled || groupContext?.groupState?.isDisabled || false;
+
+ // Determine expanded state from group or local props
+ const groupIsExpanded = groupContext
+ ? groupContext.groupState.expandedKeys.has(id)
+ : undefined;
+
+ // When disabled, force expanded to false
+ const effectiveIsExpanded = isDisabled
+ ? false
+ : groupIsExpanded ?? controlledIsExpanded;
+
+ const state = useDisclosureState({
+ isExpanded: effectiveIsExpanded,
+ defaultExpanded: isDisabled ? false : defaultExpanded,
+ onExpandedChange(expanded) {
+ if (isDisabled) return;
+
+ if (groupContext) {
+ groupContext.groupState.toggleKey(id);
+ }
+ onExpandedChange?.(expanded);
+ },
+ });
+
+ // When disabled, override state.isExpanded to false
+ const isExpanded = isDisabled ? false : state.isExpanded;
+
+ const { buttonProps, panelProps } = useDisclosure(
+ {
+ isExpanded,
+ isDisabled,
+ },
+ state,
+ panelRef,
+ );
+
+ const contextValue = useMemo(
+ () => ({
+ state,
+ buttonProps,
+ panelProps,
+ panelRef,
+ isDisabled,
+ isExpanded,
+ shape,
+ transitionDuration: undefined,
+ triggerProps: groupContext?.triggerProps,
+ contentStyles: groupContext?.contentStyles,
+ }),
+ [
+ state,
+ buttonProps,
+ panelProps,
+ isDisabled,
+ isExpanded,
+ shape,
+ groupContext?.triggerProps,
+ groupContext?.contentStyles,
+ ],
+ );
+
+ const outerStyles = extractStyles(otherProps, OUTER_STYLES);
+
+ const finalStyles = useMemo(
+ () => ({
+ ...outerStyles,
+ ...styles,
+ }),
+ [outerStyles, styles],
+ );
+
+ const finalMods = useMemo(
+ () => ({
+ expanded: isExpanded,
+ disabled: isDisabled,
+ shape,
+ ...mods,
+ }),
+ [isExpanded, isDisabled, shape, mods],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+// ============================================================================
+// Compound Component Export
+// ============================================================================
+
+const _Disclosure = Object.assign(DisclosureComponent, {
+ Trigger: DisclosureTrigger,
+ Content: DisclosureContent,
+ Group: DisclosureGroup,
+ Item: DisclosureItem,
+});
+
+export { _Disclosure as Disclosure };
+export type {
+ CubeDisclosureProps as DisclosureProps,
+ CubeDisclosureTriggerProps as DisclosureTriggerProps,
+ CubeDisclosureContentProps as DisclosureContentProps,
+ CubeDisclosureGroupProps as DisclosureGroupProps,
+ CubeDisclosureItemProps as DisclosureItemProps,
+};
diff --git a/src/components/content/Disclosure/index.ts b/src/components/content/Disclosure/index.ts
new file mode 100644
index 000000000..f70617284
--- /dev/null
+++ b/src/components/content/Disclosure/index.ts
@@ -0,0 +1 @@
+export * from './Disclosure';
diff --git a/src/components/helpers/DisplayTransition/DisplayTransition.test.tsx b/src/components/helpers/DisplayTransition/DisplayTransition.test.tsx
index 493311ca7..84d8fa7cb 100644
--- a/src/components/helpers/DisplayTransition/DisplayTransition.test.tsx
+++ b/src/components/helpers/DisplayTransition/DisplayTransition.test.tsx
@@ -173,7 +173,19 @@ describe('DisplayTransition', () => {
,
);
- // Should be in exit phase, isShown=false
+ // Immediately after rerender, still in 'entered' (exit-pending internally), isShown=true
+ expect(
+ container.querySelector('[data-phase="entered"]'),
+ ).toBeInTheDocument();
+ expect(container.querySelector('[data-shown="true"]')).toBeInTheDocument();
+ expect(onRest).not.toHaveBeenCalled();
+
+ // Advance through double-rAF to reach "exit" phase
+ act(() => {
+ jest.advanceTimersByTime(50);
+ });
+
+ // Now should be in exit phase, isShown=false
expect(container.querySelector('[data-phase="exit"]')).toBeInTheDocument();
expect(container.querySelector('[data-shown="false"]')).toBeInTheDocument();
expect(onRest).not.toHaveBeenCalled();
@@ -238,26 +250,27 @@ describe('DisplayTransition', () => {
onRest.mockClear();
// Test exit flow with duration=0
+ rerender(
+
+ {({ phase, isShown }) => (
+
+ content
+
+ )}
+ ,
+ );
+
+ // Advance through double-rAF for exit-pending → exit transition
act(() => {
- rerender(
-
- {({ phase, isShown }) => (
-
- content
-
- )}
- ,
- );
- // With duration=0, exit completes immediately
- jest.advanceTimersByTime(1);
+ jest.advanceTimersByTime(50);
});
- // With duration=0, it should go directly to unmounted (exit completes instantly)
+ // With duration=0, it should go directly to unmounted (exit completes instantly after rAF)
expect(
container.querySelector('[data-phase="unmounted"]'),
).toBeInTheDocument();
@@ -381,11 +394,11 @@ describe('DisplayTransition', () => {
);
});
- // Should be in exit or unmounted (depending on timing)
+ // Should be in 'entered' (exit-pending internally), 'exit', or 'unmounted' (depending on timing)
const phaseAfterToggle = container
.querySelector('[data-phase]')
?.getAttribute('data-phase');
- expect(['exit', 'unmounted']).toContain(phaseAfterToggle);
+ expect(['entered', 'exit', 'unmounted']).toContain(phaseAfterToggle);
// Complete all transitions
act(() => {
diff --git a/src/components/helpers/DisplayTransition/DisplayTransition.tsx b/src/components/helpers/DisplayTransition/DisplayTransition.tsx
index 0aa762ff9..0cc42e0d3 100644
--- a/src/components/helpers/DisplayTransition/DisplayTransition.tsx
+++ b/src/components/helpers/DisplayTransition/DisplayTransition.tsx
@@ -10,7 +10,8 @@ import {
const AUTO_FALLBACK_DURATION = 500;
-type Phase = 'enter' | 'entered' | 'exit' | 'unmounted';
+type Phase = 'enter' | 'entered' | 'exit-pending' | 'exit' | 'unmounted';
+type ReportedPhase = 'enter' | 'entered' | 'exit' | 'unmounted';
export type DisplayTransitionProps = {
/** Desired visibility (driver). */
@@ -20,7 +21,7 @@ export type DisplayTransitionProps = {
/** Fires after enter settles or after exit completes (unmount). */
onRest?: (transition: 'enter' | 'exit') => void;
/** Fires when phase changes. */
- onPhaseChange?: (phase: Phase) => void;
+ onPhaseChange?: (phase: ReportedPhase) => void;
/** Fires when isShown (derived from phase) changes. */
onToggle?: (isShown: boolean) => void;
/** Keep calling children during "unmounted" (you decide what to render). */
@@ -31,7 +32,7 @@ export type DisplayTransitionProps = {
respectReducedMotion?: boolean;
/** Render-prop gets { phase, isShown, ref }. Bind ref to the transitioned element for native event detection. */
children: (props: {
- phase: Phase;
+ phase: ReportedPhase;
isShown: boolean;
ref: RefCallback;
}) => ReactNode;
@@ -251,6 +252,11 @@ export function DisplayTransition({
ensureEnterFlow();
} else if (current === 'enter') {
ensureEnterFlow();
+ } else if (current === 'exit-pending') {
+ // User toggled back before exit started, cancel and stay entered
+ cancelRAF();
+ clearTimer();
+ setPhase('entered');
} else {
// already "entered"
cancelRAF();
@@ -260,12 +266,13 @@ export function DisplayTransition({
if (current === 'unmounted') {
cancelRAF();
clearTimer();
- } else if (current !== 'exit') {
- setPhase('exit');
- ensureExitFlow();
- } else {
+ } else if (current !== 'exit' && current !== 'exit-pending') {
+ // Set intermediate phase to trigger re-render, RAF will be scheduled from layout effect
+ setPhase('exit-pending');
+ } else if (current === 'exit') {
ensureExitFlow();
}
+ // 'exit-pending' is handled in useLayoutEffect below
}
return () => {
@@ -275,22 +282,37 @@ export function DisplayTransition({
};
}, [targetShown, dur, onRestEvent]);
- // OPTIONAL belt-and-suspenders: if we render while still "enter", re-arm enter flow.
- // You can remove this if you want fewer moving parts; double-rAF usually suffices.
+ // Schedule RAF from layout effect for both enter and exit-pending to ensure symmetric timing
useLayoutEffect(() => {
if (phaseRef.current === 'enter') {
ensureEnterFlow();
+ } else if (phaseRef.current === 'exit-pending') {
+ // Schedule RAF for exit, mirroring the enter flow timing
+ nextPaint(() => {
+ if (phaseRef.current === 'exit-pending') {
+ setPhase('exit');
+ ensureExitFlow();
+ }
+ });
}
return cancelRAF;
}, [phase]);
- // Call onPhaseChange when phase changes
+ // Map internal phase to reported phase (exit-pending is reported as 'entered')
+ const reportedPhase: ReportedPhase =
+ phase === 'exit-pending' ? 'entered' : phase;
+
+ // Call onPhaseChange when reported phase changes
+ const prevReportedPhaseRef = useRef(reportedPhase);
useLayoutEffect(() => {
- onPhaseChangeEvent?.(phase);
- }, [phase, onPhaseChangeEvent]);
+ if (prevReportedPhaseRef.current !== reportedPhase) {
+ prevReportedPhaseRef.current = reportedPhase;
+ onPhaseChangeEvent?.(reportedPhase);
+ }
+ }, [reportedPhase, onPhaseChangeEvent]);
// Render-time boolean (true only when visually shown)
- const isShownNow = phase === 'entered';
+ const isShownNow = phase === 'entered' || phase === 'exit-pending';
const prevIsShownRef = useRef(isShownNow);
// Call onToggle when isShown changes
@@ -318,9 +340,9 @@ export function DisplayTransition({
if (phase === 'unmounted' && !exposeUnmounted) return null;
return children({
phase:
- phase === 'enter' && duration !== undefined && !duration
+ reportedPhase === 'enter' && duration !== undefined && !duration
? 'entered'
- : phase,
+ : reportedPhase,
isShown: isShownNow,
ref: refCallback,
});
diff --git a/src/index.ts b/src/index.ts
index 2de666b58..66913cef9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -63,6 +63,15 @@ export { Suffix } from './components/layout/Suffix';
export type { CubeSuffixProps } from './components/layout/Suffix';
export { Divider } from './components/content/Divider';
export type { CubeDividerProps } from './components/content/Divider';
+export { Disclosure } from './components/content/Disclosure';
+export type {
+ CubeDisclosureProps,
+ CubeDisclosureTriggerProps,
+ CubeDisclosureContentProps,
+ CubeDisclosureGroupProps,
+ CubeDisclosureItemProps,
+ DisclosureStateContext,
+} from './components/content/Disclosure';
export { GridProvider } from './components/GridProvider';
export type { CubeGridProviderProps } from './components/GridProvider';
export { Content } from './components/content/Content';
diff --git a/src/tokens.ts b/src/tokens.ts
index a0b6bfba0..6752b2edc 100644
--- a/src/tokens.ts
+++ b/src/tokens.ts
@@ -76,6 +76,7 @@ const TOKENS = {
'leaf-sharp-radius': '0px',
'fade-width': '32px',
transition: '80ms',
+ 'disclosure-transition': '120ms',
'min-dialog-size': 'min(288px, calc(100vw - (2 * var(--gap))))',
'clear-color': 'transparent',
'border-opaque-color': 'rgb(227 227 233)',