# Card deck

A stack of cards with one on top and the next two peeking behind it: suggestions to go through one at a time, a queue to triage. Previous and Next go round the deck, as do the arrow keys on the deck (Right is next, and Left in a right-to-left page), and each step is announced with the card's name and where it is ("Invoice 1042, 2 of 5"). With `onDismiss`, dragging the top card past 40% of its width, or Delete on the deck, dismisses it: the deck calls `onDismiss` with its key and the app takes it out of `items`; without it, a drag that far goes to the next card. Backspace doesn't (since 5.0.0): to most people it means "go back" or "delete a character". Once the app has taken the card out, the deck announces it with the new top card ("Invoice 1041 dismissed. Invoice 1042, 1 of 3"), once; a card still in `items` once the deck has the app's answer is a dismissal the app refused, and is never announced. An app that takes the card out after an `await` returns that promise from `onDismiss`, and its answer is in when the promise settles. The deck's keys are declared in `aria-keyshortcuts` and in a hint, read with the deck and shown while it has keyboard focus ("Delete to dismiss, Left and right arrows to move."); the hint's line is kept while it is hidden, so showing it doesn't move what is below, and a screen reader's browse mode finds it while hidden. A press becomes a drag only once the pointer has moved 6 pixels, so a click on a button or link in a card is a click. The cards behind are only a picture of the deck: they are `inert` and hidden from assistive technology until they reach the top. With one card, Previous and Next are locked and say why.

- Registry name: `card-deck` (`@candor/card-deck`)
- 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/card-deck

## Install

```bash
npx candor-ui add card-deck
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/card-deck`.
It brings in: bidi, button, locale, locked, motion, tokens, use-announcer, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { CardDeck } from "@/components/candor/ui/card-deck"

type Suggestion = { id: string; title: string; why: string }

const suggestions: Suggestion[] = [
  { id: "s1", title: "Archive 14 stale records", why: "Not opened in 18 months and owned by people who have left." },
  { id: "s2", title: "Merge two duplicate accounts", why: "Same name, address and tax number." },
  { id: "s3", title: "Turn on two-step sign-in for admins", why: "3 of 5 admins sign in with a password only." },
  { id: "s4", title: "Shorten the export retention", why: "Exports are kept for 400 days; your policy says 90." },
]

// Go through suggestions one at a time. Previous and Next (or the arrow keys on the deck) go round it, and each step
// is announced. Dragging the top card past 40% of its width, or Delete on the deck, dismisses it; once the app has
// taken it out, the deck announces it with the new top card. The deck's keys show in a hint while it has keyboard focus.
export default function Example() {
  const [items, setItems] = React.useState(suggestions)
  return (
    <CardDeck
      label="Suggestions"
      className="w-full max-w-sm"
      items={items}
      keyOf={(s) => s.id}
      labelOf={(s) => s.title}
      onDismiss={(id) => setItems((list) => list.filter((s) => s.id !== id))}
      renderCard={(s) => (
        <div className="flex min-h-28 flex-col gap-1">
          <p className="font-medium">{s.title}</p>
          <p className="text-sm text-muted-foreground">{s.why}</p>
        </div>
      )}
    />
  )
}
```

## <CardDeck>

Props:

- `items: T[]`: The cards, in order.
- `keyOf: (item: T) => string`: Each card's key, stable while it is in the deck.
- `labelOf: (item: T) => string`: Each card's name, announced when it comes to the top. Shown as text, never as markup.
- `renderCard: (item: T) => React.ReactNode`: Draws a card's face.
- `onDismiss?: ((key: string) => void | PromiseLike<unknown>) | undefined`: Called with the top card's key when the person dismisses it; take it out of `items` in the same update, or, when that waits on something (a save), return the promise and take it out before it resolves. A card still there once the deck has the answer (the redraw, or the promise settling) stays, and its dismissal isn't announced; so does one whose promise rejects. Left out, a card can't be dismissed.
- `label: string`: What the deck is: the name of its group, "Suggestions".
- `className?: string | undefined`: Classes for the root, merged with Candor's.

## Strings (`cardDeck`)

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

- `previous`: Previous
- `next`: Next
- `count`: {n} of {total}
- `position`: {card}, {n} of {total}
- `dismissed`: {card} dismissed
- `dismissedNext`: {card} dismissed. {next}, {n} of {total}
- `keysMoveDismiss`: Delete to dismiss, Left and right arrows to move.
- `keysMove`: Left and right arrows to move.
- `keysDismiss`: Delete to dismiss.
- `empty`: No cards left.
- `onlyOne`: There's only one card.

## Motion

- **cycle** (move): When the person goes to the next or previous card, or one is dismissed, the cards behind move up into place and the top card goes to the back. Under reduced motion or the calm level they jump to their places. A deck drawn with its cards is drawn still.
- **drag** (clock): While the person drags the top card, it follows the pointer, painted from the pointer itself; let go short of the threshold, it moves back into place.

Set with `--cd-motion-card-deck-<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`.
