# 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: 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>

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>

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.

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

## <DrawerDescription>

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>

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>

No props.

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

## <DrawerOverlay>

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>

No props.

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

## <DrawerTitle>

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>

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

- **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`.
