# Hold to confirm

A button a person holds down before a risky step runs: the pointer, or Space or Enter, kept down for `holdMs` (1.2 seconds unless the app says otherwise). The bar under it fills with the real time held, from the clock, and letting go early cancels and says so. Nothing is armed in advance, no single key fires it, and it's never focused for the person. If `locked` is set while a hold is under way, the hold is cancelled and `onConfirm` is not called; `onConfirm` and `holdMs` are read when the hold ends, so the latest of each applies. Say what the step does and whether it can be undone in `reason`; it's read with the button.

Some people can't hold a key or a pointer down. Offer them another way to take the same step (TypeToConfirm, or a plain confirmation) rather than making this the only path.

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

## Install

```bash
npx candor-ui add hold-to-confirm
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/hold-to-confirm`.
It brings in: button, locale, locked, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { HoldToConfirm } from "@/components/hold-to-confirm"
import { Button } from "@/components/ui/button"

export default function Example() {
  const [done, setDone] = React.useState(false)
  if (done)
    return (
      <div className="flex flex-col items-start gap-2 text-sm">
        <p>The export is being sent to 240 people.</p>
        <Button variant="outline" size="sm" onClick={() => setDone(false)}>
          Reset the example
        </Button>
      </div>
    )
  return (
    <div className="flex flex-col gap-6">
      <HoldToConfirm
        className="max-w-sm"
        label="Send to 240 people"
        reason="Sends the export to every member of the Sales team. Sent email can’t be recalled."
        onConfirm={() => setDone(true)}
      />
      <HoldToConfirm
        className="max-w-sm"
        variant="destructive"
        label="Delete the staging database"
        reason="Removes staging-eu’s database and its 14 scheduled jobs. It can be restored from last night’s backup, not from today’s."
        locked="Only an admin can delete a database."
        onConfirm={() => {}}
      />
    </div>
  )
}
```

## <HoldToConfirm>

Props:

- `label: string`: The step, as a verb: "Delete 3 rows", "Send to 240 people".
- `reason: React.ReactNode`: What the step does and whether it can be undone, in the app's words. Shown under the button and read with it.
- `holdMs?: number | undefined` (default `1200`): How long the hold lasts, in milliseconds. 1200 unless the app says otherwise.
- `onConfirm: () => void`: Called once, when the hold has lasted `holdMs`.
- `locked?: string | undefined`: Why the viewer can't take this step at all. The button stays visible and says so, and no hold starts.
- `variant?: "default" | "destructive" | undefined` (default `"default"`): "destructive" for removal, "default" for a serious change that isn't one.
- `className?: string | undefined`

## Strings (`holdToConfirm`)

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

- `hold`: Press and hold for {duration}. Letting go early cancels.
- `progress`: Holding
- `cancelled`: Cancelled: let go before the hold finished. Nothing was done.
- `confirmed`: Confirmed: {label}.

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