# Build the "Escalation to human" pattern: `HumanHandoff`

## 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
A clear transition from agent to person, with the switch of actor made obvious.

Use it when: Support, sales and any flow where the agent's ceiling is reached mid-conversation.

## What to build
The chat pieces that move a conversation from an AI agent to a person: an always-visible "Talk to a person" control, an inline offer when the agent reaches its limit, and a clear marker showing who is responding now. It renders in a support chat header and message stream.

### Props
- `status`: "agent" | "offered" | "requested" | "queued" | "connected" | "unavailable" | "ended", controlled by the parent.
- `agent`: { name: string }, always labelled as an AI agent.
- `human?`: { name: string; role: string; avatarUrl?: string }, the person who joined.
- `queue?`: { since: Date; estimateMinutes?: number; position?: number }.
- `offerReason?`: string, why the agent is offering, e.g. "A billing specialist has to verify a new card".
- `carriesOver`: string[], what the person will see, e.g. "This conversation", "The refund already started".
- `fallback?`: { label: string; description: string }, e.g. an email callback when nobody is available.
- `onRequest()`, `onDecline()`, `onCancel()` and `onFallback()`: callbacks. The component never routes anything itself.

### States it must render
- Agent: the header names the agent with an "AI agent" label and shows Talk to a person from the first message.
- Offered: an inline card with the reason and two equal choices, Bring in a person and Keep chatting.
- Queued: "Connecting you to Billing, about 3 min" with what will carry over, and Cancel.
- Connected: a divider in the stream ("A billing specialist joined. You're now talking to a person.") and the header switches name, avatar and label.
- Unavailable: no one free right now, with the fallback offered; never a dead end.
- Ended: a divider saying the person left and who replies next.

### Interactions
- Talk to a person in the header calls `onRequest` at any point, without needing the agent to fail first.
- Bring in a person calls `onRequest`; Keep chatting calls `onDecline` and closes the card.
- Cancel while queued calls `onCancel`; the fallback button calls `onFallback`.

### Edge cases
- The person asks for a human in free text: the parent sets `requested` and the card confirms rather than arguing.
- Queue timeout: the parent moves to `unavailable`; the component never quietly returns to the agent.
- A request while the agent is mid-reply: the request is accepted at once and shown in the stream.
- The agent rejoins after the person leaves: a second divider makes that switch just as visible.

## Must avoid
Hiding the human option until the user has failed three times and lost the thread entirely.

## Engineering constraint
Route by risk and deny by default on timeout; carry the transcript and tool results into the ticket.

## Accessibility
- Announce each change of who is responding ("Now talking to a billing specialist, a person").
- The header control has a stable accessible name and stays in the same place in tab order.

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