# Loader

A loader for work the app is waiting on, with its words always beside it ("Loading reports"). It turns only while the work is running and has been heard from: past `staleAfter` without a report it stops and says "Not heard from since 10:42" (announced once, to a screen reader), and when `status` says the work is over or isn't running it stops and says so in the status's word. It never turns for a state it can't vouch for, and it isn't for an agent typing (that is `Message`'s caret) or for progress that is measured (that is `Progress`).

The shape is `shape`, else the brand's `--cd-draw-loader`, else a ring. The ring is the loader's own; dots, bars, grid, drive, orbit, pixel, equaliser and wave are drawn by the Pro motion-drawings item's CSS over the same nine hidden marks, so a theme changes the shape without the page changing, and without that item every shape is the ring. None of them is a measurement: the equaliser's bars are a fixed, regular pattern, never levels from data.

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

## Install

```bash
npx candor-ui add loader
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/loader`.
It brings in: locale, motion, relative-time, status, tokens, use-announcer, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { Loader } from "@/components/candor/loader"

export default function Example() {
  // The loader turns only while the work keeps reporting: press the button to send it a report, and leave it for 15
  // seconds to see it stop and say when it was last heard from.
  const [heardAt, setHeardAt] = React.useState(() => Date.now())
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Loader label="Loading reports" heardAt={heardAt} />
        <button type="button" className="self-start text-xs text-accent-text underline-offset-4 hover:underline" onClick={() => setHeardAt(Date.now())}>
          A report from the work
        </button>
      </div>
      <Loader label="Exporting the audit log" status="succeeded" />
      <div className="flex flex-wrap gap-x-6 gap-y-3">
        <Loader label="Syncing calendars" shape="dots" />
        <Loader label="Building the index" shape="bars" />
        <Loader label="Transcribing the call" shape="equaliser" />
        <Loader label="Uploading 3 files" shape="wave" />
      </div>
    </div>
  )
}
```

## <Loader>

Props:

- `label: string`: The work, in the app's words and language: "Loading reports", "Indexing 12 files". Always shown.
- `shape?: LoaderShape | undefined`: The shape: "ring", "dots", "bars", "grid", "drive", "orbit", "pixel", "equaliser" or "wave". Left out, the brand's `--cd-draw-loader` chooses, else the ring. Shapes other than the ring need the Pro motion-drawings item's CSS.
- `status?: RunStatus | undefined`: The work's status as the service last reported it. Left out, the loader turns as long as the work is heard from; any status but "running" stops it, with the status's word beside the label.
- `heardAt?: Date | number | null | undefined`: When the work last reported anything (an event, a byte, a heartbeat), as a Date or milliseconds since 1970. Left out or null, the app isn't tracking it and the loader doesn't claim it has gone quiet. A time that can't be read is no evidence the work is alive, so the loader stops.
- `staleAfter?: number | undefined` (default `STALE_AFTER`): How long the work may be silent before the loader stops and says so, in milliseconds. Defaults to 15 seconds.
- `className?: string | undefined`: Classes for the root, merged with Candor's.

## Strings (`loader`)

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

- `stale`: Not heard from since {time}
- `noUpdate`: Not heard from

## Types

### LoaderShape

The shapes a Loader draws. The ring is the loader's own; the others are drawn by the Pro motion-drawings item.

```ts
"ring" | "dots" | "bars" | "grid" | "drive" | "orbit" | "pixel" | "equaliser" | "wave"
```

### LoaderState

Whether the loader turns: running, not heard from in time (stale), over or not running (stopped), or no clock yet.

```ts
"running" | "stale" | "stopped" | "unchecked"
```

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

- **turning** (live): The shape turns while the work runs and was heard from within `staleAfter`; it stops the moment the work is over, isn't running, or goes quiet, and is still under reduced motion and at the Off level, where the label alone says the work is going on.

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