# Verify code

> A one-time code step with a carrier pigeon: Coo naps through the resend wait with the seconds in a thought bubble, and flies every new code to your mailbox.

The “check your inbox” step is a row of boxes and a resend link greyed out for thirty seconds. This one hands it to Coo, an illustrated carrier pigeon in a postman’s cap standing on a post across the stage from a red mailbox with its flag up. He nods along with every digit you type. A wrong code and he shakes his head while the boxes shake and clear; the right one closes the boxes into one pill and he puffs his chest out with two proud flaps. While the resend clock runs he naps on his post, eye shut and z’s drifting up, with a thought bubble over his head rolling the seconds down; typing wakes him to read along, and at zero the bubble pops and he hops up, ready to fly. Resend sends him off with a new envelope in his beak: he flies the route, waits over the mailbox until your send is done, pops the flag and flies home empty, and the clock starts again with whatever wait your server asked for. If the send fails he comes back with the envelope still in his beak. Underneath it is the real thing: one input with one-time-code autofill, paste and the numeric keyboard, onVerify for your check and onResend for your sender.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/verify-code.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: none
- Hairline UI components it needs (the CLI installs them too): `code-input`, `number-roll`, `status-button`, `text-morph`
- Docs and live preview: https://hairlineui.com/ui/verify-code
- Registry item (source): https://hairlineui.com/r/verify-code.json

## Usage

```tsx
import { VerifyCode } from "@/components/verify-code";

<VerifyCode
  destination={email}
  onVerify={(code) => verifyCode(email, code)}
  onResend={() => sendCode(email)}
/>
```

## With your own endpoints

Return false for a wrong code, and pass your rate limit back as retryAfter so the clock matches it.

```tsx
"use client";
import { VerifyCode } from "@/components/verify-code";

export function CheckInbox({ email, onDone }: { email: string; onDone: () => void }) {
  return (
    <VerifyCode
      destination={email}
      autoFocus
      onVerify={async (code) => {
        const res = await fetch("/api/verify", { method: "POST", body: JSON.stringify({ email, code }) });
        if (!res.ok) return false;
        setTimeout(onDone, 1200); // after Coo's flaps
      }}
      onResend={async () => {
        const res = await fetch("/api/verify/resend", { method: "POST", body: JSON.stringify({ email }) });
        if (!res.ok) throw new Error("Couldn’t send");
        return { retryAfter: Number(res.headers.get("Retry-After") ?? 30) };
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `destination` | `string` | – | Where the code went, shown in the line under the title. |
| `onVerify` | `(code: string) => Promise<boolean \| void>` | – | Check the code. Resolve (or return true) to accept; return false or throw for a wrong code. |
| `onResend` | `() => Promise<{ retryAfter?: number } \| void>` | – | Send a new code. Return { retryAfter } in seconds to set the next wait; throw if it didn’t go. |
| `channel` | `"email" \| "sms"` | `"email"` | The title says inbox or number. |
| `length` | `number` | `6` | How many digits. |
| `resendAfter` | `number` | `30` | Seconds before the first resend, and between resends when onResend doesn’t say. |
| `onChangeDestination` | `() => void` | – | Shows Change beside the address. |
| `title / name / autoFocus` | `string / string / boolean` | `"Check your inbox" / "Coo" / false` | The heading, the pigeon’s name in his lines, and whether the code field takes focus on mount. |

## States

- **napping**: Coo stands on his post at the left of the lit stage, the red mailbox with its flag up at the right and his dashed route between them. While the resend clock runs he dozes: his eye shuts, a z drifts up from his cap every 1.1s, and a thought bubble rolls the seconds down; the line reads “Coo is catching his breath”.
- **typing**: Typing wakes him for 4s (“Coo is reading along”): each digit rises into its box and he nods forward from his feet.
- **checking**: With every box full the row carries a sweep of light and the line reads “Coo is checking”.
- **wrong**: onVerify returns false or throws: Coo shakes his head, the boxes shake, turn red and clear from the right; the line reads “Not the code Coo carried”.
- **right**: The boxes close into one pill with a drawn check, Coo puffs his chest with two flaps and a hop, and the resend line folds away; “Signed for. You’re in.”
- **ready**: At zero the bubble shrinks away into a blur, he wakes with a hop, the line reads “Coo is ready to fly again” and “Resend in 0:00” turns into Resend code.
- **resend**: Coo takes off with an envelope (1s along the route, flapping between two wing poses), waits over the mailbox until onResend settles, pops the flag and flies home empty; Sent, then the clock starts again.
- **send failed**: onResend throws: he turns back with the envelope still in his beak, the line says “Coo couldn’t get through” and Resend shakes into Try again.

## What it handles

A verify step usually leaves people staring at a disabled Resend link with no idea how long is left or whether anything is happening. This block shows the wait, shows the send, and says plainly when a send didn’t go through.

- **Real code input**: One input with one-time-code autofill, paste and the numeric keyboard; the boxes are only what you see.
- **A wait you can see**: Coo naps through the resend wait with the seconds rolling down in a thought bubble over his head.
- **Your server’s wait**: onResend can return { retryAfter } and the next clock uses it.
- **Sends that fail**: If onResend throws, Coo comes back with the envelope and Resend turns into Try again.
- **Wrong and right**: A wrong code shakes and clears the boxes; the right one closes them into a pill with a drawn check.
- **Email or text**: channel sets the wording (inbox or number), and Change lets people fix the address.

## Replaces

an OTP input, a resend countdown, resend button states, a wrong-code shake, a verified state

## Where it fits

- **Email sign-up**: Confirm the address with the code you just sent, before the account opens.
- **Passwordless sign-in**: The magic-code step of an email or SMS login.
- **Two-factor**: A code by text message before a sensitive change.
- **Changing an email**: Prove the new address is yours before it’s saved.

## Notes

- The code field is Code input: one real input with autocomplete="one-time-code", so SMS autofill, paste and password managers work; its label and status line say what happened.
- The countdown’s digits (in the bubble and the resend line) are hidden from screen readers; they hear that a resend is coming and when it’s sent or failed.
- Coo is decoration (aria-hidden). He, the mailbox and the post are illustrations shipped inside coo-art.ts as WebP data URIs (50KB of images), so the install is complete; his nods, nap, flight, flaps, blink, z’s and the mailbox flag are code.
- One frame loop outside React moves him and sleeps when he’s still; his breathing is a compositor animation. The scene is drawn at 364 × 176 and scaled to the stage.
- With reduced motion he doesn’t fly or nod: the flag and his pose change in place.
- Built from Code input, Number roll, Status button and Text morph, which the CLI installs with it. No animation library.

License: MIT (https://hairlineui.com/license#components)
