A verification code is one value typed in several places. `PinInput` gives it a box per
character, then packs what it collects into one dense string, so a consumer reads `value` and
never has to reason about positions: deleting a character in the middle moves the ones after it
left rather than leaving a gap.

It owns typing, focus and paste, and nothing else. The code is a value the consumer verifies;
no authentication logic lives here.

```tsx demo="forms/pin-input-preview"
```

## Usage

```tsx
import { PinInput } from "@asheeui/web";

<PinInput
  label="Verification code"
  description="The four digits we sent to your phone."
  onComplete={verify}
/>
```

The field is controlled when `value` is given and owns the code otherwise, starting from
`defaultValue`. `onValueChange` reports every change; `onComplete` fires with the whole code once
it reaches its length, so a screen can verify from either. With a `name`, the field also renders
one hidden input carrying the whole code, which is what lets a plain form post a single field
rather than four.

## Length, mode and grouping

`length` and `mode` are the field's two rules about what it collects. `numeric` keeps digits,
`alphanumeric` keeps letters and digits, and `text` keeps anything but whitespace; the filter runs
on typing and on paste alike, so dropping a whole code in fills the boxes with the characters the
mode accepts and drops the rest.

```tsx demo="forms/pin-input-length"
```

`separatorAfter` draws a dash after a given box, which is how a six-digit code reads as two
groups of three. It is presentational: the dash is hidden from assistive technology, and it is
ignored when it would fall after the last box.

## Masking and failure

`masked` makes each box a password field, for a code that must not be readable over a shoulder.
The edge of the boxes and the tone of the message are two separate props: `isInvalid` colours the
boxes and marks them invalid, and `status` decides the tone of the message below them.

```tsx demo="forms/pin-input-masked"
```

That separation is the point. A code that is being checked is not a code that failed, so a field
can report `status="default"` with a message while leaving the boxes neutral, and can mark a
refused code without also claiming the reader typed something wrong in the field's own
vocabulary. `isDisabled` dims the boxes and stops the field accepting input while the label, the
description and the message stay readable.

## Props

```props PinInput
```

`length`, `mode`, `size`, `masked`, `isDisabled` and `isInvalid` are registered by the component
as `4`, `numeric`, `md`, `false`, `false` and `false`. `radius` is not registered: it resolves
from the application's `defaultRadius`, so the boxes round with the rest of the form. When the
application states no radius, the field's own fallback is `sm`, tighter than a text field's
`md`, because a box the size of a character should not be a pill.

`groupLabel` defaults to `Verification code`. It names the group when the field has no visible
label, and it prefixes the name of every box.

## Accessibility

- The boxes are one `<fieldset>`. With a `label`, the group is named by `aria-labelledby` pointing
  at the visible label; without one it takes `groupLabel` as `aria-label`. The component never
  moves focus by itself, the ref is the group, so a screen that knows why can focus it.
- Every box names itself with its position, `Verification code, character 2 of 4`, because four
  one-character inputs are otherwise announced as four anonymous fields.
- Typing a character advances to the next box, <kbd>Backspace</kbd> clears the box and steps
  back, <kbd>Delete</kbd> clears without moving, and the arrow keys, <kbd>Home</kbd> and
  <kbd>End</kbd> move without changing the code. A pasted code fills from the caret onward, and
  the first box asks the platform for `one-time-code` autofill.
- `isInvalid` marks every box `aria-invalid` and colours its edge, so a refused code is never
  signalled by colour alone; `status="error"` puts the message in a live region.

The label, description and message behave as the [field anatomy](/concepts/fields) describes.

## Related

```related input, search-input, form
```
