# Input OTP

Individual code inputs with character filtering and asynchronous verification feedback

Page: https://sui.draco.dev/docs/components/input-otp

### Example: input-otp-demo

```tsx
"use client";

import { Button } from "@workspace/ui/components/button";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  type InputOTPStatus,
} from "@workspace/ui/components/input-otp";
import { Label } from "@workspace/ui/components/label";
import { useEffect, useId, useRef, useState } from "react";
import type { ExampleProps } from "../types";

export default function Example({
  locale,
  mask = false,
}: ExampleProps & { mask?: boolean }) {
  const chinese = locale === "zh-CN";
  const id = useId();
  const [value, setValue] = useState("");
  const [status, setStatus] = useState<InputOTPStatus>("idle");
  const [result, setResult] = useState("");
  const inputRef = useRef<HTMLInputElement>(null);
  const request = useRef(0);
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  useEffect(
    () => () => {
      request.current++;
      clearTimeout(timer.current);
    },
    [],
  );
  function verify(code: string) {
    const run = ++request.current;
    clearTimeout(timer.current);
    setStatus("loading");
    setResult(chinese ? "正在验证" : "Verifying code");
    const verification = new Promise<boolean>((resolve) => {
      timer.current = setTimeout(() => resolve(code === "123456"), 1100);
    });
    void verification.then((valid) => {
      if (request.current !== run) return;
      setStatus(valid ? "success" : "error");
      const messages = chinese
        ? {
            success: "验证成功",
            error: "验证失败，输入已保留，可修改或重新输入",
          }
        : {
            success: "Verified",
            error:
              "Verification failed. Your code is preserved for editing or retry.",
          };
      setResult(messages[valid ? "success" : "error"]);
    });
  }
  return (
    <div className="grid w-full max-w-sm justify-items-center gap-4">
      <p className="text-center text-muted-foreground text-sm">
        {chinese
          ? "输入 123456 查看成功效果，其他六位数字显示失败"
          : "Enter 123456 to succeed; any other six digits show the error feedback."}
      </p>
      <Label htmlFor={id} className="sr-only">
        {chinese ? "验证码" : "Verification code"}
      </Label>
      <InputOTP
        id={id}
        length={6}
        mask={mask}
        value={value}
        onValueChange={(value) => {
          setValue(value);
          setResult("");
        }}
        onValueComplete={verify}
        status={status}
        onStatusChange={setStatus}
      >
        <InputOTPGroup>
          {[0, 1, 2, 3, 4, 5].map((index) => (
            <InputOTPSlot
              key={index}
              ref={index === 0 ? inputRef : undefined}
              className="size-10 text-lg"
            />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <p
        role="status"
        className="min-h-5 text-center text-muted-foreground text-sm"
      >
        {result}
      </p>
      <div className="flex gap-2">
        <Button
          type="button"
          variant="outline"
          size="sm"
          disabled={status === "loading" || status === "error"}
          onClick={() => {
            request.current++;
            clearTimeout(timer.current);
            setValue("");
            setStatus("idle");
            setResult("");
            inputRef.current?.focus();
          }}
        >
          {chinese ? "重新输入" : "Enter again"}
        </Button>
        <Button
          type="button"
          variant="secondary"
          size="sm"
          disabled={status !== "idle" || value.length !== 6}
          onClick={() => {
            inputRef.current?.focus();
            verify(value);
          }}
        >
          {chinese ? "重试验证" : "Retry verification"}
        </Button>
      </div>
    </div>
  );
}
```

Input OTP combines real character inputs with paste, autofill, keyboard navigation, and verification feedback. The first example accepts `123456` as a successful verification and keeps the check visible. Other six-digit codes show failure, then unfold back into editable inputs with the entered value preserved.

## Installation

```bash
bunx --bun shadcn@latest add @sui/input-otp
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. The component uses Base UI OTP Field.

## Usage

```tsx
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId } from "react";

function VerificationCode() {
  const id = useId();

  return (
    <Field>
      <FieldLabel htmlFor={id}>Verification code</FieldLabel>
      <InputOTP id={id} length={6} aria-describedby={`${id}-description`}>
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
      <FieldDescription id={`${id}-description`}>
        Enter the six-digit verification code you received.
      </FieldDescription>
    </Field>
  );
}
```

Render one `InputOTPSlot` per character in `length`. Slots register in DOM order, including across groups. `InputOTPGroup` arranges slots and `InputOTPSeparator` provides a visual divider; neither consumes a character.

## Values and completion

Use `value` and `onValueChange` for a controlled value, or `defaultValue` for internal state. `onValueComplete` reports a completed code so the application can verify it. Completion means every character is entered; it does not prove that the code is valid and does not automatically enable success feedback.

```tsx
const [value, setValue] = useState("");

<InputOTP
  length={6}
  value={value}
  onValueChange={setValue}
  onValueComplete={(code) => verifyCode(code)}
>
  <InputOTPGroup>
    {[0, 1, 2, 3, 4, 5].map((position) => (
      <InputOTPSlot key={position} />
    ))}
  </InputOTPGroup>
</InputOTP>;
```

The standard change and completion callbacks receive the value and Base UI event details. A complete pasted code can trigger completion again, allowing verification to retry.

### Example: input-otp-controlled

```tsx
"use client";

import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import * as React from "react";

export default function InputOTPControlled() {
  const [value, setValue] = React.useState("");
  const id = React.useId();

  return (
    <Field className="w-fit gap-2">
      <FieldLabel htmlFor={id}>One-time password</FieldLabel>
      <InputOTP id={id} length={6} value={value} onValueChange={setValue}>
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
      <output className="block text-center text-sm" aria-live="polite">
        {value === "" ? (
          <>Enter your one-time password.</>
        ) : (
          <>You entered: {value}</>
        )}
      </output>
    </Field>
  );
}
```

## Verification feedback

The application controls `status` and owns the verification request:

| Status | Behavior |
| --- | --- |
| `idle` | Editable inputs; filling them does not mark verification successful |
| `loading` | Inputs become read-only and collapse into a spinning indicator while the request runs |
| `success` | The indicator morphs into a check with a halo and particles; the check stays visible and inputs remain collapsed |
| `error` | The indicator morphs into an X, then fades out as the inputs unfold back to their original positions |

After your request resolves, set `success` or `error`. Success remains displayed until your application explicitly sets `status="idle"`, such as when the user chooses to enter a new code.

Error holds the X for `feedbackDuration`, then fades it out while the slots move back out to their original positions over about `380` milliseconds. The field stays read-only during this reverse transition. Once it finishes, editing and eligible focus are restored without changing the value or mask setting. `onStatusChange` requests `idle` only after error restoration. Connect it to your status setter so retry controls become available too.

```tsx
const id = useId();
const [status, setStatus] = useState<InputOTPStatus>("idle");
const feedback = {
  idle: "Enter your verification code",
  loading: "Verifying code",
  success: "Code verified",
  error: "Verification failed. Try again",
};

<Field>
  <FieldLabel htmlFor={id}>Verification code</FieldLabel>
  <InputOTP
    id={id}
    length={6}
    value={value}
    onValueChange={setValue}
    status={status}
    onStatusChange={setStatus}
    onValueComplete={async (code) => {
      setStatus("loading");
      try {
        const valid = await verifyCode(code);
        setStatus(valid ? "success" : "error");
      } catch {
        setStatus("error");
      }
    }}
  >
    {slots}
  </InputOTP>
  <FieldDescription role="status" aria-live="polite">
    {feedback[status]}
  </FieldDescription>
</Field>;
```

Import `InputOTPStatus` from the same component module. Keep request cancellation and stale-response handling in the application; the live example cancels its local timer on unmount and ignores superseded requests.

`feedbackDuration` defaults to `1250` milliseconds for the error result hold and does not time out `loading`. Error unfolds after that hold; success keeps its check until your application resets the status.

Reduced motion disables movement and particles while preserving the result hold. Error then returns to inputs without the animated transition; success still keeps its check. Timers complete the lifecycle when animation styles are unavailable. Zero duration skips the result hold. Restoration respects explicit `disabled` and `readOnly` settings. After an error, focus returns to the first slot when the field owned focus before feedback and the user has not moved focus elsewhere.

## Character filtering

`validationType` controls accepted characters. `numeric` is the default; use `alpha` for ASCII letters, `alphanumeric` for ASCII letters and digits, or `none` for custom rules. Spaces are removed and the result is limited to `length`.

Use `normalizeValue` for transformations such as uppercasing. It runs after built-in filtering, and the returned result is filtered again. Keep it idempotent. With `validationType="none"`, it can supply the custom filtering rule. `onValueInvalid` reports rejected typed or pasted characters; `inputMode` supplies a keyboard hint, not a verification result.

```tsx
<InputOTP
  length={6}
  validationType="alphanumeric"
  normalizeValue={(value) => value.toUpperCase()}
>
  {slots}
</InputOTP>;
```

### Example: input-otp-pattern

```tsx
"use client";

import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId as usePreviewId } from "react";

export function InputOTPPattern() {
  const previewId = usePreviewId();

  return (
    <Field className="w-fit">
      <FieldLabel htmlFor={`${previewId}-digits-only`}>Digits Only</FieldLabel>
      <InputOTP
        id={`${previewId}-digits-only`}
        length={6}
        validationType="numeric"
      >
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}

export default InputOTPPattern;
```

## Groups and separators

Divide a code into readable groups while keeping the same total slot count.

### Example: input-otp-separator

```tsx
import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId } from "react";

export default function InputOTPWithSeparator() {
  const id = useId();
  return (
    <Field className="w-fit">
      <FieldLabel htmlFor={id}>Verification code</FieldLabel>
      <InputOTP id={id} length={6}>
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}
```

## Disabled and read-only

`disabled` blocks interaction. `readOnly` prevents editing while preserving the current value. Loading and result feedback make the field read-only without clearing its value. Success stays collapsed until the application explicitly resets its status.

### Example: input-otp-disabled

```tsx
import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId } from "react";

export function InputOTPDisabled() {
  const id = useId();
  return (
    <Field className="w-fit">
      <FieldLabel htmlFor={id}>Disabled verification code</FieldLabel>
      <InputOTP id={id} length={6} disabled value="123456">
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}

export default InputOTPDisabled;
```

## Invalid fields

Use `aria-invalid` on the slots and `data-invalid` on the shared `Field` for persistent invalid styling. The shared `Field` handles layout and labels; use the underlying Base UI Field API when you need its form validation context. `status="error"` controls temporary verification feedback and does not replace the form's invalid state.

### Example: input-otp-invalid

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import * as React from "react";

export function InputOTPInvalid() {
  const [value, setValue] = React.useState("000000");
  const id = React.useId();

  return (
    <Field data-invalid className="w-fit">
      <FieldLabel htmlFor={id}>Verification code</FieldLabel>
      <InputOTP id={id} length={6} value={value} onValueChange={setValue}>
        <InputOTPGroup>
          <InputOTPSlot aria-invalid aria-describedby={`${id}-error`} />
          <InputOTPSlot aria-invalid aria-describedby={`${id}-error`} />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot aria-invalid aria-describedby={`${id}-error`} />
          <InputOTPSlot aria-invalid aria-describedby={`${id}-error`} />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot aria-invalid aria-describedby={`${id}-error`} />
          <InputOTPSlot aria-invalid aria-describedby={`${id}-error`} />
        </InputOTPGroup>
      </InputOTP>
      <FieldDescription id={`${id}-error`}>
        The verification code is invalid. Try another code.
      </FieldDescription>
    </Field>
  );
}

export default InputOTPInvalid;
```

## Four digits

Use `length={4}` and four slots for a numeric PIN.

### Example: input-otp-four-digits

```tsx
"use client";

import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId } from "react";

export function InputOTPFourDigits() {
  const id = useId();
  return (
    <Field className="w-fit">
      <FieldLabel htmlFor={id}>Four-digit PIN</FieldLabel>
      <InputOTP id={id} length={4} validationType="numeric">
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}

export default InputOTPFourDigits;
```

## Alphanumeric codes

Use `validationType="alphanumeric"` for codes containing letters and digits.

### Example: input-otp-alphanumeric

```tsx
"use client";

import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId } from "react";

export function InputOTPAlphanumeric() {
  const id = useId();
  return (
    <Field className="w-fit">
      <FieldLabel htmlFor={id}>Alphanumeric verification code</FieldLabel>
      <InputOTP id={id} length={6} validationType="alphanumeric">
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot />
          <InputOTPSlot />
          <InputOTPSlot />
        </InputOTPGroup>
      </InputOTP>
    </Field>
  );
}

export default InputOTPAlphanumeric;
```

## Masked entry

Set `mask` to use Base UI's native password presentation for the character inputs. This hides the displayed characters; the controlled value, callbacks, and submitted form value still contain the real code. Masking does not encrypt it.

The masked example runs the same verification flow. A successful check stays visible; after an error, the restored inputs retain their code and continue displaying masked characters.

### Example: input-otp-masked

```tsx
"use client";

import type { ExampleProps } from "../types";
import InputOTPExample from "./input-otp-demo";

export default function Example(props: ExampleProps) {
  return <InputOTPExample {...props} mask />;
}
```

## Forms

`name` submits the combined code through Base UI's hidden validation input. Use `required` for required entry. `autoSubmit` defaults to `false`; enabling it requests submission of the owning form on completion, independently of whether server verification succeeds.

### Example: input-otp-form

```tsx
"use client";

import { Button } from "@workspace/ui/components/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@workspace/ui/components/card";
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { RefreshCwIcon } from "lucide-react";
import { useId, useRef, useState } from "react";
import type { ExampleProps } from "../types";

export function InputOTPForm({ locale }: ExampleProps) {
  const id = useId();
  const chinese = locale === "zh-CN";
  const [value, setValue] = useState("");
  const [submitted, setSubmitted] = useState<string | null>(null);
  const firstInput = useRef<HTMLInputElement>(null);

  return (
    <form
      className="mx-auto w-full max-w-md"
      onSubmit={(event) => {
        event.preventDefault();
        const code = new FormData(event.currentTarget).get("verification-code");
        setSubmitted(String(code ?? ""));
      }}
    >
      <Card>
        <CardHeader>
          <CardTitle>{chinese ? "验证登录" : "Verify your login"}</CardTitle>
          <CardDescription>
            {chinese
              ? "输入发送至以下邮箱的验证码："
              : "Enter the verification code sent to your email address: "}
            <span className="font-medium">m@example.com</span>.
          </CardDescription>
        </CardHeader>
        <CardContent>
          <Field>
            <div className="flex items-center justify-between">
              <FieldLabel htmlFor={id}>
                {chinese ? "验证码" : "Verification code"}
              </FieldLabel>
              <Button
                type="button"
                variant="outline"
                size="xs"
                onClick={() => {
                  setValue("");
                  setSubmitted(null);
                  firstInput.current?.focus();
                }}
              >
                <RefreshCwIcon />
                {chinese ? "重新输入" : "Enter again"}
              </Button>
            </div>
            <InputOTP
              id={id}
              length={6}
              name="verification-code"
              required
              validationType="numeric"
              value={value}
              onValueChange={(next) => {
                setValue(next);
                setSubmitted(null);
              }}
            >
              <InputOTPGroup className="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
                <InputOTPSlot ref={firstInput} />
                <InputOTPSlot />
                <InputOTPSlot />
              </InputOTPGroup>
              <InputOTPSeparator className="mx-2" />
              <InputOTPGroup className="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
                <InputOTPSlot />
                <InputOTPSlot />
                <InputOTPSlot />
              </InputOTPGroup>
            </InputOTP>
            <FieldDescription>
              {chinese
                ? "填写全部六位数字后提交。验证码通过原生表单字段提交"
                : "Complete all six digits before submitting. The code is submitted as a native form field."}
            </FieldDescription>
          </Field>
        </CardContent>
        <CardFooter>
          <Field>
            <Button type="submit" className="w-full">
              {chinese ? "提交验证码" : "Submit code"}
            </Button>
            <output
              aria-live="polite"
              className="min-h-5 text-center text-muted-foreground text-sm"
            >
              {submitted !== null &&
                `${chinese ? "已提交验证码：" : "Submitted code: "}${submitted}`}
            </output>
          </Field>
        </CardFooter>
      </Card>
    </form>
  );
}

export default InputOTPForm;
```

## Labels, refs, and direction

`InputOTP` renders a `div`; its `ref` points to that root. Each `InputOTPSlot` renders a real `input` and accepts an `HTMLInputElement` ref, native input attributes, and Base UI input state styling. Use a slot ref to focus or inspect a character input.

Compose a visible label with the shared `Field`, `FieldLabel`, and `useId`, as in the usage example. Alternatively, import `Label` from `@workspace/ui/components/label` and associate its `htmlFor` with the `InputOTP` `id`. The shared `Field` provides layout and label composition; it is distinct from Base UI Field's validation context. Slots preserve Base UI position semantics and native ARIA attributes. The component does not generate labels or status text. Loading sets `aria-busy`; the application displays and announces feedback through `FieldDescription`, `FieldError`, or `role="status"`, as in the verification example.

Use the shared `DirectionProvider` from `@workspace/ui/components/direction` with `direction="rtl"` for right-to-left navigation and layout.

### Example: input-otp-rtl

```tsx
"use client";

import { DirectionProvider } from "@workspace/ui/components/direction";
import { Field, FieldLabel } from "@workspace/ui/components/field";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@workspace/ui/components/input-otp";
import { useId as usePreviewId } from "react";
import { type Translations, useTranslation } from "./support";

const translations: Translations = {
  en: {
    dir: "ltr",
    values: {
      verificationCode: "Verification code",
    },
  },
  ar: {
    dir: "rtl",
    values: {
      verificationCode: "رمز التحقق",
    },
  },
  he: {
    dir: "rtl",
    values: {
      verificationCode: "קוד אימות",
    },
  },
};

export function InputOTPRtl() {
  const previewId = usePreviewId();

  const { dir, t } = useTranslation(translations, "ar");

  return (
    <DirectionProvider direction={dir}>
      <Field className="mx-auto max-w-xs" dir={dir}>
        <FieldLabel htmlFor={`${previewId}-input-otp-rtl`}>
          {t.verificationCode}
        </FieldLabel>
        <InputOTP length={6} dir={dir} id={`${previewId}-input-otp-rtl`}>
          <InputOTPGroup>
            <InputOTPSlot />
            <InputOTPSlot />
            <InputOTPSlot />
            <InputOTPSlot />
            <InputOTPSlot />
            <InputOTPSlot />
          </InputOTPGroup>
        </InputOTP>
      </Field>
    </DirectionProvider>
  );
}

export default InputOTPRtl;
```

## API reference

### InputOTP

| Prop | Type | Default / behavior |
| --- | --- | --- |
| `length` | `number` | `6`; number of character slots |
| `value`, `defaultValue` | `string` | Controlled value or initial internal value |
| `onValueChange` | `(value, details) => void` | Base UI value change callback |
| `onValueComplete` | `(value, details) => void` | Reports completed entry; does not verify it |
| `status` | `InputOTPStatus` | `"idle"`; controlled verification feedback |
| `onStatusChange` | `(status: InputOTPStatus) => void` | Requests `"idle"` after error restoration |
| `feedbackDuration` | `number` | `1250` ms; error result hold before unfolding |
| `mask` | `boolean` | `false`; hides characters without changing the real value |
| `glass` | `boolean` | `false`; enables the shared material for slots |

`InputOTPGroup` accepts div props. `InputOTPSeparator` accepts Base UI separator props. `InputOTPSlot` accepts Base UI OTP input props and an `HTMLInputElement` ref. Slot order determines position.

The module exports `InputOTPProps` and `InputOTPStatus`. Character filtering, form options, native input attributes, state styling, and event detail types follow the [Base UI OTP Field API](https://base-ui.com/react/components/otp-field#api-reference).

- [Documentation](https://base-ui.com/react/components/otp-field)
