# Build the "Action approval gate" pattern: `ApprovalRequest`

## Stack (edit this block to match your project)
- React 18+ with TypeScript: one function component, typed props, no `any`.
- Tailwind CSS, using your design system's tokens if you have them.
- Presentational only: data comes in through props, decisions go out through callbacks. No fetching or global state inside.
- If your codebase already has conventions (naming, shared components, tokens), follow them over this spec.

## What the pattern is for
The agent pauses before a consequential step and presents a decision-ready packet to a human.

Use it when: Any irreversible or externally visible action: send, publish, delete, charge, deploy, grant access.

## What to build
A card the agent shows when it has paused before a consequential action. It gives a person everything needed to decide in one place (what will happen, why, the evidence, and what happens if it is wrong), then returns a single decision. It renders inline in a run log, chat or inbox.

### Props
- `action`: string, the action in plain words, e.g. "Send a $1,240 refund to the customer".
- `reason`: string, why the agent wants to do it.
- `evidence`: Array<{ label: string; detail: string }>, the facts the agent relied on.
- `ifWrong`: string, the consequence and reversibility, e.g. "Reversible for 24 hours, then final".
- `expiresAt?`: Date, when the request lapses, and `defaultOnExpiry`: "deny" | "approve" (default "deny").
- `editable?`: { label: string; value: string; validate?: (v: string) => string | null }, one value the approver may change before approving (amount, recipient, date).
- `risk`: "low" | "medium" | "high", which changes emphasis only and never hides information.
- `status`: "pending" | "editing" | "submitting" | "approved" | "rejected" | "expired", controlled by the parent.
- `decidedBy?`: { name: string; at: Date; editedValue?: string }, shown once decided.
- `onApprove(editedValue?: string)`, `onReject(reason?: string)` and `onEditStart()`: callbacks. The component never performs the action itself.

### States it must render
- Pending: action, reason, evidence and "if wrong" are visible before the buttons, with a live countdown when `expiresAt` is set.
- Editing: the editable value as an input with inline validation, plus Confirm and Cancel.
- Submitting: buttons disabled with a busy indicator; a second click does nothing.
- Approved: collapses to a one-line receipt (who, when, and the edited value next to the original if it changed).
- Rejected: a one-line receipt with the optional reason.
- Expired: a one-line receipt saying it was denied (or approved) by default at the deadline.

### Interactions
- Approve, Edit and Reject are separate, clearly labelled buttons. Approve is the primary one; Reject is never styled as primary.
- Edit swaps the buttons for the input. Confirm calls `onApprove(value)` only when `validate` returns null.
- Reject may ask for a short optional reason before calling `onReject`.
- The countdown ticks every second, but expiry is applied through the `status` the parent sets, not by the component acting on its own.

### Edge cases
- Very long action text or evidence lists: wrap, never truncate the action; collapse evidence beyond 4 items behind "Show all".
- `expiresAt` already in the past on mount: render Expired immediately.
- The parent sets `status` back to pending after a failed submit: show an inline error and re-enable the buttons.
- Several requests in one list: each keeps its own state and focus.

## Must avoid
Gating everything until reviewers rubber-stamp — oversight in appearance, none in substance.

## Engineering constraint
Approval must happen before side effects, and the run must resume from the same state, not restart.

## Accessibility
- The card is a `region` labelled by the action text. Don't announce the countdown every second; announce at 5 minutes, 1 minute and expiry.
- After a decision, move focus to the receipt line so keyboard users know what happened.

## House rules
- Real `<button>`/`<input>` elements, full keyboard use, visible focus; announce status changes with `aria-live="polite"`; respect `prefers-reduced-motion`.
- All user-facing text comes from props (or a `labels` prop) so it can be translated.
- One consistent accent marks anything the AI made or is doing; amber means "needs a person", red means risky, green means done. Never rely on colour alone.
- Fits a 360 px wide container without horizontal scrolling.

## Acceptance checks
- [ ] Every state above can be rendered by passing props, with no internal fake data.
- [ ] Every interaction and edge case above behaves as described.
- [ ] Nothing in "Must avoid" can happen with this component.

## Deliver
1. The component file with its exported prop types.
2. A short usage example that renders each state.
