# Drawer

A panel that slides in from an edge of the screen and closes with a swipe, for touch-first layouts.

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@zenoxai/ui/components/drawer"

function DrawerDemo() {
  return (
    <Drawer showSwipeHandle>
      <DrawerTrigger render={<Button variant="outline" />}>
        Triage alert
      </DrawerTrigger>
      <DrawerContent>
        <div className="mx-auto w-full max-w-md">
          <DrawerHeader>
            <DrawerTitle>Triage alert</DrawerTitle>
            <DrawerDescription>
              Brute-force login attempt on vpn-gw-02.prod
            </DrawerDescription>
          </DrawerHeader>
          <p className="p-4 text-text-secondary">
            214 failed logins from 3 addresses in the last 10 minutes. The
            account was not locked.
          </p>
          <DrawerFooter>
            <Button>Escalate to incident</Button>
            <DrawerClose render={<Button variant="outline" />}>
              Mark as false positive
            </DrawerClose>
          </DrawerFooter>
        </div>
      </DrawerContent>
    </Drawer>
  )
}
```

## Usage

```tsx
import { Button } from "@zenoxai/ui/components/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@zenoxai/ui/components/drawer"
```

```tsx
<Drawer showSwipeHandle>
  <DrawerTrigger render={<Button variant="outline" />}>
    Triage alert
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Triage alert</DrawerTitle>
      <DrawerDescription>
        Brute-force login attempt on vpn-gw-02.prod
      </DrawerDescription>
    </DrawerHeader>
    <div className="p-4">{/* body */}</div>
    <DrawerFooter>
      <Button>Escalate to incident</Button>
      <DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

A drawer is a dialog with gestures. The user can drag it to close it, snap it between heights
and stack one on top of another. Use it where people work with their thumbs: mobile layouts,
bottom sheets with quick actions, a summary that expands to full height.

- On desktop, a panel that slides in from the side and needs no gestures is a
  [Sheet](/components/sheet). Prefer it there: it is simpler and sized for forms.
- For a short task in the middle of the screen, use [Dialog](/components/dialog).
- To confirm a destructive action, use [Alert Dialog](/components/alert-dialog).

`showSwipeHandle` draws the grab bar. Show it whenever the drawer can be swiped, so the gesture
is discoverable. `DrawerFooter` stacks its buttons full width, primary action first. Give the
body `p-4`; without a body, the footer keeps the same 1rem gap below the header by itself. A bottom
drawer spans the whole viewport width; on wide screens, wrap its content in
`mx-auto w-full max-w-md` so lines and buttons stay readable.

## Examples

### Direction

`swipeDirection` is the direction that dismisses the drawer, and so the edge it enters from:
`down` (the default, a bottom sheet), `up`, `left` or `right`. Side drawers take three quarters
of the width on small screens and 24rem from the `sm` breakpoint up.

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@zenoxai/ui/components/drawer"

function DrawerDirections() {
  return (
    <>
      {(["up", "right", "down", "left"] as const).map((direction) => (
        <Drawer key={direction} swipeDirection={direction} showSwipeHandle>
          <DrawerTrigger render={<Button variant="outline" />}>
            Swipe {direction}
          </DrawerTrigger>
          <DrawerContent>
            {/* flex-1 flex-col keeps the footer at the bottom of side drawers;
                max-w-md keeps bottom and top drawers readable on wide screens. */}
            <div className="mx-auto flex w-full max-w-md flex-1 flex-col">
              <DrawerHeader>
                <DrawerTitle>Swipe {direction} to close</DrawerTitle>
                <DrawerDescription>
                  The drawer enters from the edge it is dismissed toward.
                </DrawerDescription>
              </DrawerHeader>
              <DrawerFooter>
                <DrawerClose render={<Button variant="outline" />}>
                  Close
                </DrawerClose>
              </DrawerFooter>
            </div>
          </DrawerContent>
        </Drawer>
      ))}
    </>
  )
}
```

### Snap points

`snapPoints` lets a bottom drawer rest at preset heights. Numbers from 0 to 1 are fractions of
the viewport height, larger numbers are pixels, and strings accept `px` and `rem`. The drawer
opens at the first point. Control the current point with `snapPoint` and `onSnapPointChange`.

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "@zenoxai/ui/components/drawer"

function DrawerSnapPoints() {
  return (
    <Drawer snapPoints={["10rem", 1]} showSwipeHandle>
      <DrawerTrigger render={<Button variant="outline" />}>
        Open asset summary
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>srv-db-01.prod</DrawerTitle>
          <DrawerDescription>
            Drag up for the full summary, down to close.
          </DrawerDescription>
        </DrawerHeader>
        <dl className="mx-auto grid w-full max-w-md grid-cols-2 gap-x-4 gap-y-3 p-4">
          <dt className="text-text-secondary">IP address</dt>
          <dd>10.20.4.17</dd>
          <dt className="text-text-secondary">Operating system</dt>
          <dd>Ubuntu 22.04 LTS</dd>
          <dt className="text-text-secondary">Tenant</dt>
          <dd>Acme Corp</dd>
          <dt className="text-text-secondary">Open alerts</dt>
          <dd>3</dd>
          <dt className="text-text-secondary">Last seen</dt>
          <dd>2 minutes ago</dd>
        </dl>
      </DrawerContent>
    </Drawer>
  )
}
```

### Nested

A drawer opened from inside another stacks on top of it; the one behind scales down and dims.
Use it for a confirmation or a second step that belongs to the first drawer.

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@zenoxai/ui/components/drawer"

function DrawerNested() {
  return (
    <Drawer showSwipeHandle>
      <DrawerTrigger render={<Button variant="outline" />}>
        Alert details
      </DrawerTrigger>
      <DrawerContent>
        <div className="mx-auto w-full max-w-md">
          <DrawerHeader>
            <DrawerTitle>Malware detected</DrawerTitle>
            <DrawerDescription>ws-finance-07, Acme Corp</DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <Drawer showSwipeHandle>
              <DrawerTrigger render={<Button />}>Isolate host</DrawerTrigger>
              <DrawerContent>
                <div className="mx-auto w-full max-w-md">
                  <DrawerHeader>
                    <DrawerTitle>Isolate ws-finance-07?</DrawerTitle>
                    <DrawerDescription>
                      The host loses network access except to the Zenox agent.
                    </DrawerDescription>
                  </DrawerHeader>
                  <DrawerFooter>
                    <DrawerClose render={<Button variant="destructive" />}>
                      Isolate host
                    </DrawerClose>
                    <DrawerClose render={<Button variant="outline" />}>
                      Cancel
                    </DrawerClose>
                  </DrawerFooter>
                </div>
              </DrawerContent>
            </Drawer>
            <DrawerClose render={<Button variant="outline" />}>
              Close
            </DrawerClose>
          </DrawerFooter>
        </div>
      </DrawerContent>
    </Drawer>
  )
}
```

## Accessibility

- The drawer is modal by default: focus moves into it and stays there, Escape and a click on
  the backdrop close it, and focus returns to the trigger. `modal={false}` lets the user keep
  working with the page behind it.
- Swiping is never the only way out. Keep a `DrawerClose` button in the footer for keyboard and
  screen reader users.
- Render a `DrawerTitle`: it is the drawer's accessible name. The swipe handle is hidden from
  assistive technology.

## API reference

`import { ... } from "@zenoxai/ui/components/drawer"`

### Drawer

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| actionsRef | `RefObject<DrawerRootActions \| null>` |  | A ref to imperative actions. - `unmount`: Ends the closing phase of the drawer after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the drawer completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the drawer imperatively when called. |
| defaultOpen | `boolean` | `false` | Whether the drawer is initially open.  To render a controlled drawer, use the `open` prop instead. |
| defaultSnapPoint | `DrawerSnapPoint \| null` |  | The initial snap point value when uncontrolled. |
| defaultTriggerId | `string \| null` |  | ID of the trigger that the drawer is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open drawer. |
| disablePointerDismissal | `boolean` | `false` | Whether to prevent the drawer from closing on outside presses. For non-modal drawers, this also prevents the drawer from closing when focus moves outside of it. |
| handle | `DrawerHandle<unknown>` |  | A handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method. |
| modal | `boolean \| "trap-focus"` | `true` | Determines if the drawer enters a modal state when open. - `true`: user interaction is limited to just the drawer: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the drawer, but document page scroll is not locked and pointer interactions outside of it remain enabled. |
| onOpenChange | `((open: boolean, eventDetails: DrawerRootChangeEventDetails) => void)` |  | Event handler called when the drawer is opened or closed. |
| onOpenChangeComplete | `((open: boolean) => void)` |  | Event handler called after any animations complete when the drawer is opened or closed. |
| onSnapPointChange | `((snapPoint: DrawerSnapPoint \| null, eventDetails: DrawerRootSnapPointChangeEventDetails) => void)` |  | Callback fired when the snap point changes. |
| open | `boolean` |  | Whether the drawer is currently open. |
| showSwipeHandle | `boolean` | `false` | Shows a grab bar on the drawer's inner edge. Defaults to false. |
| snapPoint | `DrawerSnapPoint \| null` |  | The currently active snap point. Use with `onSnapPointChange` to control the snap point. |
| snapPoints | `DrawerSnapPoint[]` |  | Snap points used to position the drawer. Use numbers between 0 and 1 to represent fractions of the viewport height, numbers greater than 1 as pixel values, or strings in `px`/`rem` units (for example, `'148px'` or `'30rem'`). |
| snapToSequentialPoints | `boolean` | `false` | Disables velocity-based snap skipping so drag distance determines the next snap point. |
| swipeDirection | `"left" \| "right" \| "up" \| "down"` | `down` | The swipe direction used to dismiss the drawer. |
| triggerId | `string \| null` |  | ID of the trigger that the drawer is associated with. This is useful in conjunction with the `open` prop to create a controlled drawer. There's no need to specify this prop when the drawer is uncontrolled (that is, when the `open` prop is not set). |

### DrawerPortal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| container | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null> \| null` |  | A parent element to render the portal element into. |
| keepMounted | `boolean` | `false` | Whether to keep the portal mounted in the DOM while the popup is hidden. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerPortalState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### DrawerOverlay

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| forceRender | `boolean` | `false` | Whether the backdrop is forced to render even when nested. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerBackdropState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### DrawerSwipeHandle

No props of its own.

### DrawerTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| handle | `DrawerHandle<unknown>` |  | A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method. |
| id | `string` |  | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root `triggerId` prop). |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| payload | `unknown` |  | A payload to pass to the drawer when it is opened. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerTriggerState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### DrawerClose

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerCloseState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### DrawerContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| finalFocus | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null)` |  | Determines the element to focus when the drawer is closed.  - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`).   Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
| initialFocus | `boolean \| RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| void \| HTMLElement \| null)` |  | Determines the element to focus when the drawer is opened.  - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`).   Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerPopupState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### DrawerHeader

No props of its own.

### DrawerFooter

No props of its own.

### DrawerTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerTitleState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### DrawerDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DrawerDescriptionState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

