# Locked

The honest-state rule: a control that cannot be used says why.

Wrap any single control. With a `reason`, the control stays focusable but is marked `aria-disabled`, its activation is blocked (click, Enter, Space), and it is described by the reason — so keyboard and screen-reader users reach the explanation on the control itself. The reason shows as a tooltip, or printed under the control with `inline`. Pressing the control reveals the reason (the tooltip is held open, the printed reason is emphasised) and, under data-motion="expressive" only, shakes it briefly; with reduced motion or the default subtle preset, only the reason is shown.

- Registry name: `locked` (`@candor/locked`)
- Tier: Free (MIT). No licence key needed.
- Docs: https://candor.themethodic.com/docs/locked

## Install

```bash
npx candor-ui add locked
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/locked`.
It brings in: tooltip, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { Button } from "@/components/ui/button"
import { Locked } from "@/components/locked"

export default function Example() {
  return (
    <div className="flex flex-wrap items-start gap-6">
      <Locked reason="Only an admin can publish this.">
        <Button>Publish</Button>
      </Locked>
      <Locked inline reason="You wrote this change, so someone else must approve it.">
        <Button variant="outline">Approve</Button>
      </Locked>
    </div>
  )
}
```

## <Locked>

Props:

- `reason?: string | null | undefined`: Why the control can't be used. Without one, the control renders as normal.
- `inline?: boolean | undefined` (default `false`): Print the reason under the control instead of in a tooltip.
- `children: Lockable`
- `className?: string | undefined`

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