# Cart Drawer

> An animated cart drawer for React and shadcn, with undoable removals and a free-shipping progress bar.

The bag as a swipe-to-dismiss sheet: steps roll each line and the subtotal, a removed line folds away behind an Undo Pill, and the free-shipping bar fills as the bag crosses it.

- Tier: Eased Pro
- Interaction: Drag
- Page: https://eased.dev/components/cart-drawer
- Installs: motion

## Install

Cart Drawer ships with Eased Pro (https://eased.dev/#pricing). Members add the `@eased-pro` registry with their token (https://eased.dev/account), then run:

```bash
npx shadcn add @eased-pro/cart-drawer
```

The bag as a sheet on Sheet (Base UI Drawer): rolling line and subtotal figures, undoable removals and a free-shipping bar.

## When to use

- The bag on any storefront: open it from the header and after Add to bag.
- When shoppers adjust quantities before checkout and need totals to stay in view.

## When not to use

- The checkout itself: use a page; a drawer is for review, not payment.
- A one-item purchase flow (tickets, donations): go straight to checkout.

## Usage

```tsx example.tsx
import { CartDrawer } from "@/components/eased/cart-drawer/cart-drawer";

<CartDrawer
  open={open}
  onOpenChange={setOpen}
  lines={lines}
  onLinesChange={setLines}
  onRemoveCommit={(removed) => api.remove(removed.map((l) => l.id))}
  freeShippingAt={500}
  discount={{ code: "FIELD15", off: 0.15 }}
  onCheckout={() => router.push("/checkout")}
/>
```

## API reference

### CartDrawer

| Prop | Type | Default | Description |
|---|---|---|---|
| `open` / `defaultOpen` / `onOpenChange` | `boolean` | | |
| `lines` / `defaultLines` / `onLinesChange` | `CartDrawerLine[]` | | `{ id, name, detail?, price, compareAt?, qty, maxQty?, image? }`. Removals leave `lines` at once. |
| `onRemoveCommit` | `(lines) => void` | | After the 5s undo window. |
| `freeShippingAt` | `number` | | Shows how far off the threshold the bag is. |
| `discount` | `{ code, off }` | | `off` is a share of the subtotal. |
| `onCheckout` | `(lines) => void` | | |
| `container` | `HTMLElement \| null` | `document.body` | Portal target; a `contain: layout` box keeps the drawer inside it. |
| `modal` | `boolean \| "trap-focus"` | `true` | `"trap-focus"` skips the scroll lock. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Side width from 640px; below that it is a bottom sheet. |
| `footnote` | `ReactNode` | | A line under Checkout, e.g. "Shipping and taxes calculated at checkout". |
| `defaultSummaryOpen` | `boolean` | `false` | With a discount, start with Subtotal and Discount shown above Total. Closed, Total shows the pre-discount figure struck. |

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Steppers (at 1 the minus becomes Remove), Total, Checkout; focus stays in the drawer. |
| `Enter` / `Space` on Total | Shows or hides Subtotal and Discount. |
| `Escape` | Closes it; focus returns to the trigger. |
| `⌘ Z` / `Ctrl Z` | Undoes a removal while the pill shows. |

## Accessibility

- Titled "Bag" with the item count; the shipping line describes it.
- Quantities are live outputs; every money figure has a plain-text copy for screen readers.

## Motion

Opens on Sheet's 500ms enter curve and follows the finger back to its edge. Steps roll the line total, the discount and the subtotal on the snappy spring. Total unfolds Subtotal and Discount on Auto Height. A removed line fades and shrinks 4% in 150ms while the lines below glide up (FLIP), and an Undo Pill drains for 5s. The shipping bar fills on the smooth spring and turns success at the threshold. Reduced motion: no slide, opacity only.

## Notes for AI

Pick it for the storefront bag. Hold `lines` in your state and commit removals in `onRemoveCommit`, not `onLinesChange`, so Undo needs no server round trip.

Don't add your own confirm dialog for Remove: the undo window replaces it (rule 73). Don't compute totals for display; pass prices and quantities.

## Related

- [Sheet](/components/sheet): the drawer underneath.
- [Undo Pill](/components/undo-pill): the removal window.
- [Product Card](/components/product-card): fills the bag.
