# Confirm button

A button that asks for a second press before a step that is hard to take back: "Remove item", then "Press again to confirm". The first press arms it (said aloud); a second, separate press within `armedFor` (long enough to hear that and press again, unless the app says otherwise) runs `onConfirm` (the second click of a double-click, or Enter held down, doesn't); Escape, moving focus away or the time running out disarms it, and says so. The done label shows only once `onConfirm`'s promise resolves; if it rejects, the reason is shown and said, and the button disarms. It is one element throughout, so focus stays on it.

`undo` says what can and can't be undone, in the app's words; it is shown under the button and is its description in every state. For a step that can't be taken back at all, pair it with `HoldToConfirm` or `TypeToConfirm` instead.

`form` says how the label changes: `confirm` (the default) crossfades it, `roll` rolls the next words up, `slide` slides the old label off, and `morph` also turns the armed button into the destructive fill.

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

## Install

```bash
npx candor-ui add confirm-button
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/confirm-button`.
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 { ConfirmButton } from "@/components/candor/ui/confirm-button"
import { Button } from "@/components/candor/ui/button"

/*
 * The removals here stand in for your own service: the done label waits for its answer. The second one is refused,
 * and the button shows why. For a step that can't be taken back at all, use HoldToConfirm or TypeToConfirm instead.
 */
const remove = (ok: boolean) => new Promise<void>((resolve, reject) => setTimeout(() => (ok ? resolve() : reject(new Error("They still own 2 running agents. Hand those over first."))), 700))

export default function Example() {
  const [run, setRun] = React.useState(0)
  return (
    <div className="flex flex-col gap-6">
      <ConfirmButton key={`a-${run}`} label="Remove item" doneLabel="Removed" undo="You can restore it from Trash for 30 days." onConfirm={() => remove(true)} />
      <ConfirmButton
        key={`b-${run}`}
        form="roll"
        variant="outline"
        label="Remove member"
        confirmLabel="Press again to remove"
        doneLabel="Removed"
        undo="You can invite them again; their past work stays."
        onConfirm={() => remove(false)}
      />
      <Button variant="outline" size="sm" className="self-start" onClick={() => setRun((n) => n + 1)}>
        Reset the examples
      </Button>
    </div>
  )
}
```

## <ConfirmButton>

Props:

- `label: string`: The step, as a verb: "Remove item", "Revoke key".
- `confirmLabel?: string | undefined`: The label once armed. Defaults to "Press again to confirm".
- `doneLabel?: string | undefined`: The label once `onConfirm` has resolved: "Removed". Defaults to "Done".
- `undo: string`: What can and can't be undone, in the app's words: "You can restore it from Trash for 30 days." Shown and read with the button.
- `onConfirm: () => Promise<void> | void`: Runs the step on the second press. The done label waits for its promise; a rejection's message is shown as the reason.
- `form?: "confirm" | "roll" | "slide" | "morph" | undefined` (default `"confirm"`): How the label changes: `confirm` (the default), `roll`, `slide` or `morph`.
- `armedFor?: number | undefined`: How long it stays armed after the first press, in milliseconds. Left out, it is long enough to hear the armed announcement at a slow screen-reader rate and then press again, in whole seconds and never under 4 (6 seconds for the English words); the announcement says how long. Set, it is the app's: give at least as long.
- `variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | undefined` (default `"destructive"`): The button's look: `destructive` (the default) for removal, or another of Button's variants.
- `size?: "default" | "xs" | "sm" | "lg" | undefined`: The button's size, as Button's.
- `locked?: string | null | undefined`: Why the viewer can't take this step. The button stays focusable, says why, and doesn't arm. `null` or left out: unlocked.
- `className?: string | undefined`: Classes for the wrapper round the button and its words.

## Strings (`confirmButton`)

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

- `confirm`: Press again to confirm
- `done`: Done
- `disarmed`: Not confirmed. Nothing was done.
- `failed`: Couldn't do it: {reason}
- `failedNoReason`: Couldn't do it. No reason was given.
- `armed`: {confirm}. It disarms in {seconds}.

## Motion

- **done** (mark, draw): The tick is drawn along its path when `onConfirm`'s promise resolves, beside the done label.
- **label** (value, fade): With `form="confirm"` or `"morph"`, the label crossfades to the next words when the person arms the button, when it disarms and when the step is done. The label it is drawn with is shown still.
- **roll** (value, slide): With `form="roll"`, the next words roll up into place as the old ones go, when the person arms the button, when it disarms and when the step is done. Under reduced motion they crossfade.
- **slide** (move): With `form="slide"`, the old label slides off and the next one slides into its place, when the person arms the button, when it disarms and when the step is done. Under reduced motion the label is simply replaced.
- **working** (live): While `onConfirm`'s promise is pending, a spinner turns beside the label. Under reduced motion it is still, with its words.

Set with `--cd-motion-confirm-button-<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`.
