# Drawer

A panel that slides in from an edge of the screen over the page, for a task or detail that doesn't need a full dialog.

- Registry name: `drawer` (`@candor/drawer`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/drawer

## Install

```bash
npx candor-ui add drawer
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/drawer`.
It brings in: dialog, heading-scope, layer, locale, motion, tokens, utils.
npm packages: @base-ui/react@^1.9.0, lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/candor/ui/button"
import { Input } from "@/components/candor/ui/input"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/candor/ui/drawer"

export default function Example() {
  const [name, setName] = React.useState("Nightly export")
  const [saved, setSaved] = React.useState("Nightly export")
  const [open, setOpen] = React.useState(false)
  const [asked, setAsked] = React.useState(false)
  const dirty = name !== saved
  return (
    // Dragging down, Escape, a click outside and × all ask first; with unsaved changes the drawer stays and says why.
    <Drawer
      open={open}
      onOpenChange={(next) => {
        setOpen(next)
        if (!next) setAsked(false)
      }}
      onDismissAttempt={() => {
        if (!dirty) return
        setAsked(true)
        return false
      }}
    >
      <DrawerTrigger render={<Button variant="outline">Edit job</Button>} />
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Edit job</DrawerTitle>
          <DrawerDescription>Changes apply to the next run.</DrawerDescription>
        </DrawerHeader>
        <div className="flex flex-col gap-2 px-4">
          <Input aria-label="Job name" value={name} onChange={(e) => setName(e.target.value)} />
          {asked && (
            <p role="alert" className="text-sm text-destructive">
              You have unsaved changes. Save them, or discard them to close.
            </p>
          )}
        </div>
        <DrawerFooter>
          <Button
            onClick={() => {
              setSaved(name)
              setAsked(false)
              setOpen(false)
            }}
          >
            Save
          </Button>
          <DrawerClose render={
            <Button variant="outline" onClick={() => setName(saved)}>
              Discard changes
            </Button>
          } />
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}
```

## <Drawer>

Props:

- `onOpenChange?: ((open: boolean, details?: DrawerPrimitive.Root.ChangeEventDetails | undefined) => void) | undefined`: Called when the drawer opens or closes, with Base UI's details when Base UI closed it (Escape, a click outside).
- `onDismissAttempt?: ((attempt: { reason: DrawerDismissReason }) => boolean | void) | undefined`: Called when the person tries to close the drawer by Escape, a click outside, the close button or a drag, before it closes. Return `false` to keep it open (and tell the person why, in your own UI); anything else lets it close. Your own `DrawerClose` buttons and a change to `open` are not attempts and always take effect.

Also takes the props of `Omit<DrawerPrimitive.Root.Props, "modal" | "onOpenChange">`.

## <DrawerClose>

A control that closes the drawer; `event.preventDefault()` in its `onClick` keeps it open, as on Radix.

Props:

- `className?: string | undefined`: Classes for the part, merged with Candor's.
- `style?: React.CSSProperties | undefined`: Inline styles for the part, merged over its motion chain.

Also takes the props of `Omit<P, "className" | "style">`.

## <DrawerContent>

The drawer's panel, with its backdrop, the close button and grab bar, dragging to close and Tab kept inside it. Content inside sits under the title, an `<h2>`: it is in a `HeadingScope` of 2, so Candor items in it draw their headings from `<h3>`.

Props:

- `className?: string | undefined`: Classes for the part, merged with Candor's.
- `style?: React.CSSProperties | undefined`: Inline styles for the part, merged over its motion chain.
- `side?: "top" | "right" | "bottom" | "left" | "start" | "end" | undefined` (default `"bottom"`): The edge the drawer slides in from, and the direction it is dragged to close. Defaults to bottom. `"start"` and `"end"` follow the reading direction (`start` is the left edge in LTR and the right edge in RTL); `"left"` and `"right"` are always that edge.
- `showHandle?: boolean | undefined`: Shows the grab bar on the edge facing the page. Defaults to true for a drawer from the top or bottom.
- `showCloseButton?: boolean | undefined` (default `true`): Shows the close (×) button. Defaults to true.
- `unsaved?: string | undefined`: What a dismissal would lose, in a few words ("your note"): while set, a dismissal (a drag past the threshold too) asks first, in a bar in the drawer.

Also takes the props of `Omit<P, "className" | "style">`.

## <DrawerDescription>

The sentence under the title that says what the drawer is for, read with its name.

Props:

- `className?: string | undefined`: Classes for the part, merged with Candor's.
- `style?: React.CSSProperties | undefined`: Inline styles for the part, merged over its motion chain.

Also takes the props of `Omit<P, "className" | "style">`.

## <DrawerFooter>

The block at the bottom of the drawer for its actions.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <DrawerHandle>

The grab bar. It is decoration for pointers: the drawer closes from the keyboard with Escape or the × button.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <DrawerHeader>

The block at the top of the drawer for its title and description.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <DrawerOverlay>

The backdrop that dims the page behind the drawer. While the panel is dragged toward its edge, the backdrop lightens with it, painted from the pointer (a share from 0 to 1 the panel sets on it as it moves).

Props:

- `className?: string | undefined`: Classes for the part, merged with Candor's.
- `style?: React.CSSProperties | undefined`: Inline styles for the part, merged over its motion chain.

Also takes the props of `Omit<P, "className" | "style">`.

## <DrawerPortal>

Puts the drawer's overlay and panel at the end of the page, or in a Candor host inside another library's dialog.

No props.

Also takes the props of `Omit<DrawerPrimitive.Portal.Props, "container">`.

## <DrawerSwipeArea>

A strip along the edge a drawer lives on, where a swipe in from that edge opens it, as on a phone. Only for a coarse pointer (a finger): it isn't drawn for a mouse or while the drawer is open, it is hidden from screen readers and takes no focus, and the drawer's trigger stays the way in for everyone. Put it inside the `Drawer`, with the same `side` as its content.

The strip is 12 px deep and sits over the page's edge. A tap on it that isn't a swipe goes through to what is under it, but a scroll that starts on it doesn't scroll the page, so keep scrolling content and small controls (a tab bar's labels, a Send button) clear of that edge, or leave the swipe area out.

Props:

- `side?: "top" | "right" | "bottom" | "left" | "start" | "end" | undefined` (default `"bottom"`): The edge the drawer comes from, as on `DrawerContent`: a swipe in from it opens the drawer.
- `className?: string | undefined`: Classes for the strip, merged with Candor's.

## <DrawerTitle>

The drawer's heading, an `<h2>`, which names it to screen readers.

Props:

- `className?: string | undefined`: Classes for the part, merged with Candor's.
- `style?: React.CSSProperties | undefined`: Inline styles for the part, merged over its motion chain.

Also takes the props of `Omit<P, "className" | "style">`.

## <DrawerTrigger>

The control that opens the drawer; focus returns to it when the drawer closes.

Props:

- `className?: string | undefined`: Classes for the part, merged with Candor's.
- `style?: React.CSSProperties | undefined`: Inline styles for the part, merged over its motion chain.

Also takes the props of `Omit<P, "className" | "style">`.

## Strings (`drawer`)

The fixed words, English by default. Override any of them under `drawer` in `<LocaleProvider strings={{ drawer: { … } }}>`; the table is exported as `drawerStrings`.

- `close`: Close

## Types

### DrawerDismissReason

The ways a person can try to close a drawer without one of the app's own buttons.

```ts
"escape" | "outside" | "drag" | "close"
```

## Motion

- **nudge** (attention): One ring round the drawer when a press outside is kept from closing it.
- **open** (overlay, slide): The panel slides in from its edge over the base duration and out over the quick one, with the overlay fading beside it. The edge it comes from is where it lives. Under the calm level (reduced motion, or `data-motion="calm"`) it fades in place instead of sliding.
- **release** (move): The panel returns to its edge after a drag that stopped short of the threshold.

Set with `--cd-motion-drawer-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
