---
title: Accordion
description: Expandable sections for FAQs, settings, and layered details.
group: Components
order: 10
---

Stack related sections and reveal their content as needed. Keep one or several panels open, control the state yourself or leave it internal, and animate height with CSS.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionTrigger>What ships in the box?</AccordionTrigger>
        <AccordionContent>
          Source files you own. Install a component, then edit it like any
          other file in your app.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Will this lock me to one stack?</AccordionTrigger>
        <AccordionContent>
          It targets React. The rest of your tooling stays yours — bundler,
          router, and styling approach included.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>How do I pull a new piece in?</AccordionTrigger>
        <AccordionContent>
          Run the CLI against the registry. The component lands in your repo
          ready to customize.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add accordion
```

## Usage

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
```

```tsx
<Accordion type="single" collapsible defaultValue="item-1">
  <AccordionItem value="item-1">
    <AccordionTrigger>What ships in the box?</AccordionTrigger>
    <AccordionContent>
      You get the source files. Install the component, then make it yours.
    </AccordionContent>
  </AccordionItem>
</Accordion>
```

## Composition

```tree
Accordion
├── AccordionItem
│   ├── AccordionTrigger
│   └── AccordionContent
└── AccordionItem
    ├── AccordionTrigger
    └── AccordionContent
```

Give each `AccordionItem` a stable `value`. Put its label in `AccordionTrigger` and its body in `AccordionContent`.

## Basic

Keep one panel open at a time. With `collapsible`, selecting the open trigger closes it.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionTrigger>What ships in the box?</AccordionTrigger>
        <AccordionContent>
          Source files you own. Install a component, then edit it like any
          other file in your app.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Will this lock me to one stack?</AccordionTrigger>
        <AccordionContent>
          It targets React. The rest of your tooling stays yours — bundler,
          router, and styling approach included.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>How do I pull a new piece in?</AccordionTrigger>
        <AccordionContent>
          Run the CLI against the registry. The component lands in your repo
          ready to customize.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Multiple

Set `type="multiple"` to keep several panels open. In this mode, `defaultValue` and `value` accept a string array.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="multiple" defaultValue={["item-1", "item-2"]}>
      <AccordionItem value="item-1">
        <AccordionTrigger>Keyboard and screen readers</AccordionTrigger>
        <AccordionContent>
          Each trigger is a button with expanded state, and panels are exposed
          as labelled regions.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Open more than one panel</AccordionTrigger>
        <AccordionContent>
          Pass type="multiple" when sections should stay open side by side.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>Motion without extra packages</AccordionTrigger>
        <AccordionContent>
          Height animates with CSS grid rows — no Framer or Radix required.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Disabled

Add `disabled` to an `AccordionItem` when that panel should not open.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionTrigger>Available right now</AccordionTrigger>
        <AccordionContent>
          This panel opens and closes like any other item.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2" disabled>
        <AccordionTrigger>Coming in a later drop</AccordionTrigger>
        <AccordionContent>
          Disabled items stay closed and cannot be toggled.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>Where do tokens live?</AccordionTrigger>
        <AccordionContent>
          Theme values sit in your CSS variables — tweak once, every surface
          follows.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Plus / Minus

Set `indicator="plus"` on `AccordionTrigger` for a plus that becomes a minus when open. The default is `chevron`.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionTrigger indicator="plus">
          Expand with a plus mark
        </AccordionTrigger>
        <AccordionContent>
          The vertical stroke rotates into the horizontal one so plus becomes
          minus without swapping icons.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger indicator="plus">
          Same behavior, different cue
        </AccordionTrigger>
        <AccordionContent>
          Useful when a chevron feels too navigational and you want an add /
          remove hint instead.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger indicator="plus">
          Still one accordion
        </AccordionTrigger>
        <AccordionContent>
          indicator only changes the end mark — open state and content stay the
          same.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Icons

Pass `icon` to `AccordionTrigger` to place an icon before the title. Expanded content stays aligned with the title text.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionTrigger icon={<BoxIcon />}>
          Packages and installs
        </AccordionTrigger>
        <AccordionContent>
          Drop components into your project tree. No opaque runtime package
          sitting between you and the markup.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger icon={<CodeIcon />}>
          Edit the source directly
        </AccordionTrigger>
        <AccordionContent>
          Rename props, restyle triggers, or strip pieces you do not need —
          it is just React.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger icon={<SparkIcon />}>
          Shape it to your product
        </AccordionTrigger>
        <AccordionContent>
          Pair icons, copy, and layout chrome however your docs or app need.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Outlined

Add a border around the root when the group needs a clearer edge. This changes the presentation, not the behavior.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion
      type="single"
      collapsible
      defaultValue="item-1"
      className="rounded-md border border-foreground/10 px-3"
    >
      <AccordionItem value="item-1">
        <AccordionTrigger>Give it a hard edge</AccordionTrigger>
        <AccordionContent>
          A light border frames the stack when the page needs a clearer block.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Keep pieces separate</AccordionTrigger>
        <AccordionContent>
          Trigger, content, and item stay independent so chrome never leaks into
          behavior.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>Same API underneath</AccordionTrigger>
        <AccordionContent>
          Outlined is just className on the root — nothing new to learn.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Separated

Outline each item and add `gap` to `Accordion` to present the panels as separate blocks.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="single" collapsible className="gap-2" defaultValue="item-1">
      <AccordionItem
        value="item-1"
        className="rounded-md border border-foreground/10 px-3 last:!border-b"
      >
        <AccordionTrigger>Each item is its own box</AccordionTrigger>
        <AccordionContent>
          Border and radius sit on AccordionItem — the root only adds gap.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem
        value="item-2"
        className="rounded-md border border-foreground/10 px-3 last:!border-b"
      >
        <AccordionTrigger>Space between the cards</AccordionTrigger>
        <AccordionContent>
          gap-2 on Accordion keeps panels from touching without extra wrappers.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem
        value="item-3"
        className="rounded-md border border-foreground/10 px-3 last:!border-b"
      >
        <AccordionTrigger>Combine with icons or plus</AccordionTrigger>
        <AccordionContent>
          Still the same component — stack className with icon and indicator.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Card

Place the accordion in a `Card` when it needs a title and a little context.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

export function Example() {
  return (
    <Card className="w-full border-0">
      <CardHeader>
        <CardTitle>Registry & CLI</CardTitle>
        <CardDescription>
          Short answers for installing and owning ui pieces.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <Accordion type="single" collapsible defaultValue="item-1">
          <AccordionItem value="item-1">
            <AccordionTrigger>What does add actually do?</AccordionTrigger>
            <AccordionContent>
              It copies the component source into your project and wires any
              local imports it needs.
            </AccordionContent>
          </AccordionItem>
          <AccordionItem value="item-2">
            <AccordionTrigger>Can I change files after install?</AccordionTrigger>
            <AccordionContent>
              Yes. Once it is in your tree it is yours — no upstream unlock step.
            </AccordionContent>
          </AccordionItem>
          <AccordionItem value="item-3">
            <AccordionTrigger>Do I need a private registry?</AccordionTrigger>
            <AccordionContent>
              Not to start. Use the public set first; host your own when you want
              internal blocks.
            </AccordionContent>
          </AccordionItem>
        </Accordion>
      </CardContent>
    </Card>
  )
}
```

## Nested

Render another `Accordion` inside `AccordionContent` for nested sections. Add `unmountOnExit` to the parent content if nested panels should reset when it closes.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function Example() {
  return (
    <Accordion type="multiple" defaultValue={["item-1"]}>
      <AccordionItem value="item-1">
        <AccordionTrigger>Getting started</AccordionTrigger>
        <AccordionContent unmountOnExit>
          <Accordion type="multiple" className="pl-3">
            <AccordionItem value="setup">
              <AccordionTrigger>Install the package</AccordionTrigger>
              <AccordionContent>
                Pull the component with the CLI, then import it like any other
                local file.
              </AccordionContent>
            </AccordionItem>
            <AccordionItem value="theme">
              <AccordionTrigger>Wire your theme</AccordionTrigger>
              <AccordionContent>
                Map tokens once in CSS. Nested panels inherit the same surface
                and type styles.
              </AccordionContent>
            </AccordionItem>
          </Accordion>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Composition</AccordionTrigger>
        <AccordionContent unmountOnExit>
          <Accordion type="multiple" className="pl-3">
            <AccordionItem value="nest">
              <AccordionTrigger>Why nest at all?</AccordionTrigger>
              <AccordionContent>
                Put another Accordion inside AccordionContent when a section
                needs its own open state.
              </AccordionContent>
            </AccordionItem>
            <AccordionItem value="offset">
              <AccordionTrigger>Keep a small indent</AccordionTrigger>
              <AccordionContent>
                A light pl on the inner root shows hierarchy without changing
                the look of the triggers.
              </AccordionContent>
            </AccordionItem>
          </Accordion>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>Still one primitive</AccordionTrigger>
        <AccordionContent>
          Outer and inner stacks are the same Accordion — only placement
          changes.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Card grid

`AccordionContent` accepts any layout. Here, it opens into a grid of compact cards.

```tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

function MiniCard({
  title,
  description,
}: {
  title: string
  description: string
}) {
  return (
    <div className="rounded-md border border-foreground/10 p-3">
      <p className="text-sm font-medium tracking-wide text-foreground">{title}</p>
      <p className="mt-1 text-[12px] leading-relaxed tracking-wide text-foreground/50">
        {description}
      </p>
    </div>
  )
}

export function Example() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionTrigger>Starter blocks</AccordionTrigger>
        <AccordionContent>
          <div className="grid grid-cols-2 gap-2 pt-1">
            <MiniCard title="Auth shell" description="Sign-in layout with room for providers." />
            <MiniCard title="Settings" description="Account, billing, and preference sections." />
            <MiniCard title="Empty state" description="First-run copy and a single action." />
            <MiniCard title="Data table" description="Dense rows with filters above." />
          </div>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Charts & metrics</AccordionTrigger>
        <AccordionContent>
          <div className="grid grid-cols-2 gap-2 pt-1">
            <MiniCard title="Spark line" description="Inline trend without a full chart page." />
            <MiniCard title="KPI strip" description="Three numbers with quiet labels." />
            <MiniCard title="Range compare" description="This week versus last week." />
            <MiniCard title="Breakdown" description="Simple share of traffic by source." />
          </div>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>Docs patterns</AccordionTrigger>
        <AccordionContent>
          <div className="grid grid-cols-2 gap-2 pt-1">
            <MiniCard title="Install card" description="Command plus short context." />
            <MiniCard title="API strip" description="Prop name and one-line meaning." />
            <MiniCard title="Preview frame" description="Live demo with code under it." />
            <MiniCard title="Token table" description="Name, value, and where it lands." />
          </div>
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## API Reference

### Accordion

| Prop | Type | Default |
| --- | --- | --- |
| type | "single" \| "multiple" | "single" |
| collapsible | boolean | true |
| defaultValue | string \| string[] | — |
| value | string \| string[] | — |
| onValueChange | (value: string \| string[]) => void | — |
| className | string | — |

```tsx
<Accordion type="single" collapsible defaultValue="item-1">
  <AccordionItem value="item-1">
    <AccordionTrigger>Section</AccordionTrigger>
    <AccordionContent>Content</AccordionContent>
  </AccordionItem>
</Accordion>
```

### AccordionItem

| Prop | Type | Default |
| --- | --- | --- |
| value | string | — |
| disabled | boolean | false |
| className | string | — |

```tsx
<AccordionItem value="item-1">
  <AccordionTrigger>Section</AccordionTrigger>
  <AccordionContent>Content</AccordionContent>
</AccordionItem>
```

### AccordionTrigger

| Prop | Type | Default |
| --- | --- | --- |
| icon | ReactNode | — |
| indicator | "chevron" \| "plus" | "chevron" |
| className | string | — |

```tsx
<AccordionTrigger>Section</AccordionTrigger>
```

### AccordionContent

| Prop | Type | Default |
| --- | --- | --- |
| unmountOnExit | boolean | false |
| className | string | — |

```tsx
<AccordionContent>Content</AccordionContent>
```

## Source

```tsx
"use client";

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type ButtonHTMLAttributes,
  type HTMLAttributes,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

type AccordionType = "single" | "multiple";

type AccordionContextValue = {
  type: AccordionType;
  collapsible: boolean;
  openValues: string[];
  toggle: (value: string) => void;
  baseId: string;
};

const AccordionContext = createContext<AccordionContextValue | null>(null);
const AccordionItemContext = createContext<{
  value: string;
  disabled: boolean;
  hasIcon: boolean;
  setHasIcon: (value: boolean) => void;
} | null>(null);

function useAccordionContext() {
  const context = useContext(AccordionContext);
  if (!context) {
    throw new Error("Accordion components must be used within <Accordion>");
  }
  return context;
}

function useAccordionItemContext() {
  const context = useContext(AccordionItemContext);
  if (!context) {
    throw new Error("AccordionItem parts must be used within <AccordionItem>");
  }
  return context;
}

type AccordionProps = {
  type?: AccordionType;
  collapsible?: boolean;
  defaultValue?: string | string[];
  value?: string | string[];
  onValueChange?: (value: string | string[]) => void;
  className?: string;
  children: ReactNode;
};

function toArray(value?: string | string[]) {
  if (value == null) return [] as string[];
  return Array.isArray(value) ? value : [value];
}

export function Accordion({
  type = "single",
  collapsible = true,
  defaultValue,
  value,
  onValueChange,
  className,
  children,
}: AccordionProps) {
  const baseId = useId();
  const controlled = value !== undefined;
  const [uncontrolled, setUncontrolled] = useState(() => toArray(defaultValue));
  const openValues = controlled ? toArray(value) : uncontrolled;

  const setOpenValues = useCallback(
    (next: string[]) => {
      if (!controlled) setUncontrolled(next);
      if (type === "single") onValueChange?.(next[0] ?? "");
      else onValueChange?.(next);
    },
    [controlled, onValueChange, type],
  );

  const toggle = useCallback(
    (itemValue: string) => {
      if (type === "single") {
        const isOpen = openValues[0] === itemValue;
        if (isOpen && !collapsible) return;
        setOpenValues(isOpen ? [] : [itemValue]);
        return;
      }

      const isOpen = openValues.includes(itemValue);
      setOpenValues(
        isOpen
          ? openValues.filter((entry) => entry !== itemValue)
          : [...openValues, itemValue],
      );
    },
    [collapsible, openValues, setOpenValues, type],
  );

  const context = useMemo(
    () => ({ type, collapsible, openValues, toggle, baseId }),
    [baseId, collapsible, openValues, toggle, type],
  );

  return (
    <AccordionContext.Provider value={context}>
      <div className={cn("flex w-full flex-col", className)}>{children}</div>
    </AccordionContext.Provider>
  );
}

type AccordionItemProps = {
  value: string;
  disabled?: boolean;
  className?: string;
  children: ReactNode;
};

export function AccordionItem({
  value,
  disabled = false,
  className,
  children,
}: AccordionItemProps) {
  const [hasIcon, setHasIcon] = useState(false);

  return (
    <AccordionItemContext.Provider
      value={{ value, disabled, hasIcon, setHasIcon }}
    >
      <div
        data-slot="accordion-item"
        data-disabled={disabled ? "" : undefined}
        className={cn("border-b border-border last:border-b-0", className)}
      >
        {children}
      </div>
    </AccordionItemContext.Provider>
  );
}

function ChevronIndicator({ open, muted }: { open: boolean; muted?: boolean }) {
  return (
    <span
      aria-hidden="true"
      className={cn(
        "inline-flex size-4 shrink-0 items-center justify-center transition-transform duration-200 ease-out",
        muted ? "text-muted-foreground/50" : "text-muted-foreground",
        open && "rotate-180",
      )}
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        className="size-4"
      >
        <path
          d="m6 9 6 6 6-6"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
    </span>
  );
}

function PlusMinusIndicator({ open, muted }: { open: boolean; muted?: boolean }) {
  return (
    <span
      aria-hidden="true"
      className={cn(
        "relative inline-flex size-4 shrink-0 items-center justify-center",
        muted ? "text-muted-foreground/50" : "text-muted-foreground",
      )}
    >
      <span className="absolute inset-x-[3px] top-1/2 h-px -translate-y-1/2 bg-current" />
      <span
        className={cn(
          "absolute inset-y-[3px] left-1/2 w-px -translate-x-1/2 bg-current transition-transform duration-200 ease-out",
          open && "rotate-90",
        )}
      />
    </span>
  );
}

type AccordionTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  icon?: ReactNode;
  indicator?: "chevron" | "plus";
};

export function AccordionTrigger({
  className,
  children,
  icon,
  indicator = "chevron",
  ...props
}: AccordionTriggerProps) {
  const { openValues, toggle, baseId } = useAccordionContext();
  const { value, disabled, setHasIcon } = useAccordionItemContext();
  const open = openValues.includes(value);
  const triggerId = `${baseId}-trigger-${value}`;
  const contentId = `${baseId}-content-${value}`;

  useEffect(() => {
    setHasIcon(Boolean(icon));
  }, [icon, setHasIcon]);

  return (
    <div className="flex">
      <button
        type="button"
        id={triggerId}
        aria-controls={contentId}
        aria-expanded={open}
        aria-disabled={disabled || undefined}
        disabled={disabled}
        data-state={open ? "open" : "closed"}
        onClick={() => {
          if (disabled) return;
          toggle(value);
        }}
        className={cn(
          "flex flex-1 items-center gap-3 py-3 text-left text-sm font-medium tracking-wide",
          disabled ? "cursor-default text-muted-foreground/50" : "text-foreground",
          className,
        )}
        {...props}
      >
        {icon ? (
          <span
            aria-hidden="true"
            className={cn(
              "inline-flex size-4 shrink-0 items-center justify-center",
              disabled ? "text-muted-foreground/50" : "text-muted-foreground",
            )}
          >
            {icon}
          </span>
        ) : null}
        <span className="min-w-0 flex-1">{children}</span>
        {indicator === "plus" ? (
          <PlusMinusIndicator open={open} muted={disabled} />
        ) : (
          <ChevronIndicator open={open} muted={disabled} />
        )}
      </button>
    </div>
  );
}

type AccordionContentProps = HTMLAttributes<HTMLDivElement> & {
  unmountOnExit?: boolean;
};

export function AccordionContent({
  className,
  children,
  unmountOnExit = false,
  ...props
}: AccordionContentProps) {
  const { openValues, baseId } = useAccordionContext();
  const { value, hasIcon } = useAccordionItemContext();
  const open = openValues.includes(value);
  const triggerId = `${baseId}-trigger-${value}`;
  const contentId = `${baseId}-content-${value}`;
  const [mounted, setMounted] = useState(open || !unmountOnExit);
  const [mountKey, setMountKey] = useState(0);
  const wasOpen = useRef(open);

  useEffect(() => {
    if (!unmountOnExit) {
      setMounted(true);
      return;
    }

    if (open) {
      if (!wasOpen.current) {
        setMountKey((key) => key + 1);
      }
      wasOpen.current = true;
      setMounted(true);
      return;
    }

    wasOpen.current = false;
    const timeout = window.setTimeout(() => setMounted(false), 200);
    return () => window.clearTimeout(timeout);
  }, [open, unmountOnExit]);

  return (
    <div
      id={contentId}
      role="region"
      aria-labelledby={triggerId}
      data-state={open ? "open" : "closed"}
      className={cn(
        "grid transition-[grid-template-rows] duration-200 ease-out",
        open ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
      )}
      {...props}
    >
      <div className="overflow-hidden" aria-hidden={!open}>
        {mounted ? (
          <div
            key={mountKey}
            className={cn(
              "pb-3 text-[13px] leading-relaxed tracking-wide text-muted-foreground",
              hasIcon && "pl-7",
              className,
            )}
          >
            {children}
          </div>
        ) : null}
      </div>
    </div>
  );
}
```
