# Progress button

A button that fills as the work it started goes: an upload, an export, a sync. While working it fills across its width only as far as the measured `progress` (0 to 1), and never ahead of it; with `progress={null}` it doesn't fill at all, shows a spinner and says the progress is unknown. With `files`, the fill is one segment per file, each to its own progress, and the label counts them ("3 files, 1 done"), naming any that failed. It ends on the result the app sets: a tick for `done`, a cross and what failed for `failed`, announced once.

The button is one element throughout, so focus stays on it. While working it is `aria-disabled` and `aria-busy` (never `disabled`), and a press does nothing; so does one while the app sets `aria-disabled` (or wraps it in `Locked`), by pointer or keyboard, and the button stays focusable and says why. The measured percentage is its description ("40% done"), read with its name; it has no `aria-valuenow`, which a button can't carry. In forced colours the fill is drawn as a bar along its bottom edge in the text colour, since a fill behind the label would vanish.

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

## Install

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

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

## Example

```tsx
"use client"

import * as React from "react"
import { ProgressButton, type ProgressButtonFile, type ProgressButtonState } from "@/components/candor/ui/progress-button"
import { Button } from "@/components/candor/ui/button"

/*
 * The uploads here stand in for your own: each file's progress is what the upload reports, a step at a time, and the
 * button fills only that far. The last file fails, and the button names it.
 */
const start: ProgressButtonFile[] = [
  { id: "a", name: "q3-report.pdf", progress: 0, status: "running" },
  { id: "b", name: "board-notes.docx", progress: 0, status: "running" },
  { id: "c", name: "forecast.xlsx", progress: 0, status: "running" },
]

export default function Example() {
  const [state, setState] = React.useState<ProgressButtonState>("idle")
  const [files, setFiles] = React.useState<ProgressButtonFile[]>(start)
  const timer = React.useRef<ReturnType<typeof setInterval> | undefined>(undefined)
  React.useEffect(() => () => clearInterval(timer.current), [])
  const upload = () => {
    setState("working")
    setFiles(start)
    let tick = 0
    timer.current = setInterval(() => {
      tick += 1
      setFiles((was) =>
        was.map((f, i) => {
          if (f.status !== "running") return f
          const progress = Math.min(1, tick * (0.12 + i * 0.05))
          if (i === 2 && progress >= 0.6) return { ...f, progress, status: "failed" }
          return { ...f, progress, status: progress >= 1 ? "succeeded" : "running" }
        })
      )
      if (tick >= 9) {
        clearInterval(timer.current)
        setState("failed")
      }
    }, 400)
  }
  const overall = files.reduce((sum, f) => sum + (f.progress ?? 0), 0) / files.length
  return (
    <div className="flex flex-wrap items-center gap-3">
      <ProgressButton
        state={state}
        progress={overall}
        files={state === "idle" ? undefined : files}
        labels={{ idle: "Upload 3 files", working: "Uploading…", done: "Uploaded", failed: "Couldn't upload every file" }}
        onClick={upload}
      />
      <ProgressButton state="working" progress={null} labels={{ idle: "Sync", working: "Syncing…", done: "Synced" }} variant="outline" />
      {state !== "idle" && state !== "working" && (
        <Button variant="ghost" size="sm" onClick={() => setState("idle")}>
          Reset the example
        </Button>
      )}
    </div>
  )
}
```

## <ProgressButton>

Props:

- `progress: number | null`: How far the work has got, from 0 to 1, as measured; `null` when it isn't known. Never estimated.
- `state: ProgressButtonState`: Where the work is: the app's, from the real result. `done` only once the service has confirmed it.
- `labels: { /** The action, as a verb: "Upload". */ idle: string; /** While the work runs: "Uploading…". */ working: string; /** After the service confirmed it: "Uploaded". */ done: string; /** After it failed: "Couldn't upload". Defaults to "That didn't work". */ failed?: string | undefined; }`: The words for each state: the action ("Upload"), working ("Uploading…"), done ("Uploaded") and failed.
- `files?: ProgressButtonFile[] | undefined`: Several files sent by one press, each with its own measured progress and status.

Also takes the props of `Omit<React.ComponentProps<typeof Button>, "children">`.

## Strings (`progressButton`)

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

- `failed`: That didn't work
- `measured`: {percent} done
- `unknown`: {working}, progress unknown
- `files`: { one: "{count} file, {done} done", other: "{count} files, {done} done" }
- `failedFiles`: Failed: {names}

## Types

### ProgressButtonFile

One file of several sent by one press, with its own measured progress.

```ts
{
  /** A stable id. */
  id: string;
  /** The file's name, as the person knows it: "report.pdf". */
  name: string;
  /** How far it has got, from 0 to 1, as measured; `null` when it isn't known. */
  progress: number | null;
  /** Where it is: `running` while it is sent, `succeeded` once confirmed, `failed` when it didn't arrive. */
  status: RunStatus;
}
```

### ProgressButtonState

Where the work a ProgressButton starts has got to, as the app knows it.

```ts
"idle" | "working" | "done" | "failed"
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### RunStatus

Import: `import type { RunStatus } from "@/lib/candor/status"` (item `status`, https://candor.themethodic.com/docs/status.md)

The status vocabulary every Candor agent component shares. Components take plain data in these shapes rather than an SDK's message objects, so they work with your own LLM drivers, the Vercel AI SDK or anything else; adapters map an SDK's parts onto them.

```ts
"idle" // not started
 | "queued" // accepted, waiting for a worker
 | "running" // an agent is working on it now
 | "waiting" // stopped to ask a person (approval, question)
 | "blocked" // can't start: something it depends on failed or is waiting
 | "succeeded" | "failed" | "cancelled" | "skipped"
```

## Motion

- **progress** (fill): While the work runs, the button fills across its width as far as the measured progress, and moves between measured values, never ahead of them. With several files each segment fills to its own file's progress. Under reduced motion it snaps to each value.
- **result** (mark, draw): The tick (done) or the cross (failed) is drawn along its path when the app sets the result while the button is on screen. A button drawn already done or failed shows its mark at once.
- **working** (live): While the work runs and its progress isn't known, a spinner turns beside the working label in place of a fill. Under reduced motion it is still, with its words.

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