# Drawer

> La hoja que se arrastra: entra desde un borde, se cierra deslizándola y para en puntos de anclaje.

```tsx
import { Drawer, DrawerBody, DrawerClose, DrawerContent, … } from "sebs7n-ui/drawer"
```

## Ejemplos

### Filtros en mobile

La hoja de abajo es el patrón de filtros del celular: entra desde el borde, se cierra con el pulgar hacia abajo y deja ver el listado de atrás. El `DrawerFooter` fija el "Aplicar" arriba del borde, donde llega la mano.

```tsx
import { Button } from "sebs7n-ui/button"
import { DatePicker } from "sebs7n-ui/date-picker"
import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "sebs7n-ui/drawer"
import { Input } from "sebs7n-ui/input"
import { Label } from "sebs7n-ui/label"

function FiltrosEnMobile() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="secondary" />}>Filtros</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Filtrar facturas</DrawerTitle>
          <DrawerDescription>Se aplican al listado sin recargar la página.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="flex flex-col gap-4 pb-5">
          <div className="flex flex-col gap-2">
            <Label htmlFor="drawer-cliente">Cliente</Label>
            <Input id="drawer-cliente" placeholder="Acme S.A." />
          </div>
          <div className="flex flex-col gap-2">
            <Label htmlFor="drawer-desde">Desde</Label>
            <DatePicker id="drawer-desde" />
          </div>
        </DrawerBody>
        <DrawerFooter>
          <DrawerClose render={<Button />}>Aplicar</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}
```

### El detalle de un ítem, a media hoja

Con `snapPoints` la hoja para a mitad de camino: se ve el detalle sin tapar la lista, y se puede subir hasta el final. `defaultSnapPoint` elige dónde abre. El punto más alto pone `data-expanded` en el popup, por si el header tiene que cambiar ahí.

```tsx
import { Badge } from "sebs7n-ui/badge"
import { Button } from "sebs7n-ui/button"
import { Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "sebs7n-ui/drawer"

function DetalleAMediaHoja() {
  return (
    <Drawer defaultSnapPoint={0.45} snapPoints={[0.45, 1]}>
      <DrawerTrigger render={<Button variant="secondary" />}>Ver la factura 0012</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Factura 0012</DrawerTitle>
          <DrawerDescription>Acme S.A., emitida el 14 de marzo.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="flex flex-col gap-4 pb-5">
          <div className="flex items-center gap-2">
            <Badge color="green">Pagada</Badge>
            <Badge color="gray">3 ítems</Badge>
          </div>
          <dl className="flex flex-col gap-3 text-callout">
            <div className="flex justify-between">
              <dt className="text-label-secondary">Vencimiento</dt>
              <dd className="tabular-nums">13/04</dd>
            </div>
            <div className="flex justify-between">
              <dt className="text-label-secondary">Importe</dt>
              <dd className="tabular-nums">$ 128.400</dd>
            </div>
            <div className="flex justify-between">
              <dt className="text-label-secondary">Condición</dt>
              <dd>A 30 días</dd>
            </div>
          </dl>
        </DrawerBody>
        <DrawerFooter>
          <Button>Descargar el PDF</Button>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}
```

### Elegir de una lista larga

Lo que scrollea va adentro de `DrawerBody`: esa es la zona donde el dedo mueve la lista en vez de arrastrar la hoja. Si la lista estuviera suelta en el popup, cada intento de scrollear cerraría el drawer.

```tsx
import { Button } from "sebs7n-ui/button"
import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "sebs7n-ui/drawer"
import { useState } from "react"

function ElegirDeUnaListaLarga() {
  const [chofer, setChofer] = useState<string | null>(null)

  return (
    <div className="flex flex-col items-start gap-3">
      <Drawer>
        <DrawerTrigger render={<Button variant="secondary" />}>{chofer ?? "Asignar chofer"}</DrawerTrigger>
        <DrawerContent className="data-[swipe-direction=down]:max-h-[70%]">
          <DrawerHeader>
            <DrawerTitle>Asignar chofer</DrawerTitle>
            <DrawerDescription>Doce disponibles para el 14 de marzo.</DrawerDescription>
          </DrawerHeader>
          <DrawerBody className="pb-6">
            <ul className="flex flex-col">
              {CHOFERES.map((nombre) => (
                <li key={nombre}>
                  <DrawerClose
                    className="flex h-11 w-full items-center rounded-control px-2 text-left text-callout outline-none hover:bg-fill-2 focus-visible:focus-ring"
                    onClick={() => setChofer(nombre)}
                  >
                    {nombre}
                  </DrawerClose>
                </li>
              ))}
            </ul>
          </DrawerBody>
        </DrawerContent>
      </Drawer>
      {chofer && <p className="text-footnote text-label-secondary">Asignado: {chofer}</p>}
    </div>
  )
}
```

## Props

### Drawer

Hereda las props de `Drawer.Root`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `actionsRef` | `React.RefObject<DrawerRoot.Actions \| null>` | — | **Heredada de Base UI.** Ref con las acciones imperativas de Base UI (`unmount()`), para desmontarlo sin esperar la animación de salida. |
| `defaultOpen` | `boolean` | — | **Heredada de Base UI.** Si arranca abierto. Es la versión no controlada de `open`. |
| `modal` | `boolean \| "trap-focus"` | — | **Heredada de Base UI.** Con `true`, mientras está abierto el resto de la página no recibe clicks ni foco. Con `"trap-focus"` atrapa el foco pero deja pasar los clicks de afuera. |
| `onOpenChange` | `((open: boolean, eventDetails: DrawerRoot.ChangeEventDetails) => void)` | — | **Heredada de Base UI.** Se llama con el estado nuevo cada vez que se abre o se cierra. |
| `open` | `boolean` | — | **Heredada de Base UI.** Si está abierto. Pasarla lo vuelve controlado: sin `onOpenChange` ya no se cierra solo. |

### DrawerBody

Hereda las props de `Drawer.Content`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerClose

Hereda las props de `Drawer.Close`.

Sin props propias: pasa todo al primitivo.

### DrawerContent

Hereda las props de `Drawer.Popup`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `labels` | `{ close?: string }` | — | El texto del botón X. Con un `LabelsProvider` se traduce para toda la app; esta prop es la excepción de una pantalla puntual. |
| `showCloseButton` | `boolean` | `true` | El botón X de la esquina. Apagarlo deja al drawer sin control visible de cierre: si lo hacés, poné otro. |
| `showHandle` | `boolean` | `true` | La barra de arrastre. Es decoración: apagala solo si el drawer no se puede arrastrar. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |
| `finalFocus` | `boolean \| React.RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| HTMLElement \| void)` | — | **Heredada de Base UI.** Qué recibe el foco al cerrar. Por defecto, lo que lo abrió. |
| `initialFocus` | `boolean \| React.RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| HTMLElement \| void)` | — | **Heredada de Base UI.** Qué recibe el foco al abrir. Por defecto, el primer elemento tabulable de adentro. |

### DrawerDescription

Hereda las props de `Drawer.Description`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerFooter

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerHandle

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerHeader

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerSwipeArea

Hereda las props de `Drawer.SwipeArea`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerTitle

Hereda las props de `Drawer.Title`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### DrawerTrigger

Hereda las props de `Drawer.Trigger`.

Sin props propias: pasa todo al primitivo.

## Teclado

| Tecla | Qué hace |
|---|---|
| Enter · Espacio | Abre desde el trigger. |
| Escape | Cierra y devuelve el foco al trigger. Es el camino de teclado, y no se puede apagar. |
| Tab · ⇧Tab | Recorre solo el contenido del drawer: el foco queda atrapado adentro. |
| Click en el fondo | Cierra. |
| — | **No hay tecla para arrastrar.** El gesto es un atajo: el cierre accesible es Escape y el botón X. |

## Accesibilidad

- **Un drawer que solo se cierra deslizando es un drawer que no se puede cerrar.** Por eso `showCloseButton` viene prendido y Escape siempre funciona: arrastrar no es una opción con teclado, con switch control ni con una sola mano ocupada.
- El handle es `aria-hidden`: es una pista visual, no un control. No recibe foco ni se anuncia, así que nunca cuenta como la salida del drawer.
- `DrawerTitle` no es opcional: es el nombre accesible del diálogo. Sin él, aviso por consola en desarrollo.
- El movimiento pasa por `motion-reduce`: con `prefers-reduced-motion` la hoja aparece en lugar de deslizarse.
- Lo que arrastra es todo el popup menos `DrawerBody`. Un control que se maneja con el dedo adentro del área de arrastre —un slider, un canvas— necesita `data-base-ui-swipe-ignore` para que el gesto no se lo robe.

## Reglas de uso

- **El dedo lo mueve → `Drawer`. Solo se lee y se cierra → `Sheet`. Está centrado y es una decisión → `Dialog`.** Esa es toda la regla.
- **No reemplaza a `Sheet`, y `Sheet` no está construido sobre esto.** `Sheet` es un `Dialog` pegado a un borde, sin gesto; `Drawer` trae `swipeDirection`, `snapPoints`, el área de swipe y el handle. Son dos patrones, y `Sheet` ya vive en producción: darle gesto por abajo sería un cambio de comportamiento que no aparece en ningún diff.
- En desktop, casi siempre querés `Sheet`: nadie arrastra una hoja con el mouse. `Drawer` es para la mano.
- Lo que scrollea va adentro de `DrawerBody`. Es la zona donde el dedo mueve el contenido en vez de la hoja; sin eso, cada intento de scrollear cierra el drawer.
- `snapPoints` solo tiene sentido con `swipeDirection` vertical (`down` o `up`), que es el default.
- Va pegado al borde (2.0), como `Sheet`: el radio del panel solo en las esquinas de adentro y el área segura de padding del lado de la pantalla (con `viewport-fit=cover`, como en `Sheet`). El gesto, los snap points y el handle no cambian.
- El lado sale de `swipeDirection` del root, no de una prop del contenido: una sola fuente de verdad para de dónde entra y hacia dónde se descarta.
- `DrawerSwipeArea` abre con un swipe desde el borde, pero nunca va sola: sin un `DrawerTrigger` al lado, el drawer no existe para quien usa teclado.

## Relacionados

[sheet](/docs/components/sheet.md) · [dialog](/docs/components/dialog.md) · [popover](/docs/components/popover.md)
