# Build the "Contextual nudge" pattern: `ContextualNudge`

## 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
An in-place hint that an AI action is available on the object the user is already looking at.

Use it when: When the capability is buried in a menu and discovery is the actual bottleneck.

## What to build
A small, dismissible hint attached to the object a person is already looking at, offering one AI action that is known to work on that object. It states why it appeared and opens straight into the action. It renders inline next to a record, message, row or empty field.

### Props
- `eligible`: boolean, decided by the parent from a server-side check. When false the component renders nothing.
- `reason`: string, the specific trigger in plain words, e.g. "These two charges match".
- `actionLabel`: string, a verb phrase naming the one action, e.g. "Draft refund reply".
- `variant`: "inline" | "placeholder", a bordered hint beside content, or a quiet placeholder inside an empty input that disappears on typing.
- `status`: "idle" | "running" | "done" | "error", controlled by the parent after the action is taken.
- `onAccept()`: called when the person takes the action. The component never performs it itself.
- `onDismiss(scope: "this" | "type")`: dismiss for this object only, or stop showing this kind of nudge.
- `shortcut?`: string, a key hint shown beside the label when the action has one.

### States it must render
- Hidden: `eligible` is false, nothing renders and no space is reserved.
- Idle: one line with the AI accent, the reason, the action button and a dismiss control.
- Placeholder: inside an empty field, muted hint text with the shortcut; it vanishes as soon as the person types.
- Running: the action button shows progress and is disabled; dismiss stays available.
- Done: the nudge collapses away once the result is shown elsewhere; it does not linger as a badge.
- Error: a short message that the action failed on this object, with retry.

### Interactions
- The action button calls `onAccept` and nothing else; it never opens a menu of unrelated actions.
- Dismiss offers "Not for this one" and "Stop suggesting this", each calling `onDismiss` with the matching scope.
- In placeholder variant, the shortcut key calls `onAccept` only while the field is empty and focused.

### Edge cases
- Several eligible objects on one screen: the parent should pass `eligible` for at most one; the component shows no generic sparkle when it has no specific `reason`.
- Eligibility changes while visible (e.g. the record is edited): hide immediately rather than let the person open into "not available here".
- An accept that the server then rejects: show the error state on the nudge, not a modal.
- A previously dismissed object: the parent passes `eligible: false`; the component holds no memory of its own.

## Must avoid
Sprinkling sparkle icons on every surface until the badge means nothing and users tune it out.

## Engineering constraint
Gate on eligibility server-side; a nudge that opens into "not available here" is worse than no nudge.

## Accessibility
- The nudge is a `note` (or `aside`) labelled by its reason, placed after the content it refers to in reading order so it is met in context.
- Do not move focus to the nudge when it appears; announce its appearance once, politely.

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