# Drawer

A panel that slides in from an edge and can be dragged away, in Candor's theme (built on Radix's dialog, so Base UI projects get the same API). The drawer follows the pointer while it is dragged and never closes a pending decision silently: before Escape, a click outside, the close button or a drag past the threshold closes it, the app is asked through `onDismissAttempt` and can keep it open to show why.

- 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: locale, motion, render-prop, tokens, utils.
npm packages: lucide-react@^1.67.0, radix-ui@^1.8.1.

## Example

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/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 asChild>
        <Button variant="outline">Edit job</Button>
      </DrawerTrigger>
      <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 asChild>
            <Button variant="outline" onClick={() => setName(saved)}>
              Discard changes
            </Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}
```

## <Drawer>

Props:

- `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<React.ComponentProps<typeof DrawerPrimitive.Root>, "modal">`.

## <DrawerClose>

No props.

Also takes the props of `React.ComponentProps<typeof DrawerPrimitive.Close>`, `RenderProp`.

## <DrawerContent>

Props:

- `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 `React.ComponentProps<typeof DrawerPrimitive.Content>`.

## <DrawerDescription>

No props.

Also takes the props of `React.ComponentProps<typeof DrawerPrimitive.Description>`.

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

No props.

Also takes the props of `React.ComponentProps<typeof DrawerPrimitive.Overlay>`.

## <DrawerPortal>

No props.

Also takes the props of `React.ComponentProps<typeof DrawerPrimitive.Portal>`.

## <DrawerTitle>

No props.

Also takes the props of `React.ComponentProps<typeof DrawerPrimitive.Title>`.

## <DrawerTrigger>

No props.

Also takes the props of `React.ComponentProps<typeof DrawerPrimitive.Trigger>`, `RenderProp`.

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
