{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp",
  "title": "Input OTP",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A one-time-code field with grouped slots, separator, caret, paste and autofill support, invalid state and reduced-motion-safe animation, on a single real input.",
  "dependencies": [
    "input-otp@^1",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/input-otp.tsx",
      "content": "// Ballmac UI: Input OTP. https://ui.ballmac.com/components/input-otp\n// Based on shadcn/ui Input OTP (MIT, Copyright (c) 2023 shadcn) on input-otp (MIT, Copyright (c) 2023 Guilherme Rodz), adding an invalid state, a reduced-motion-safe caret, and a filled-slot pop.\n\"use client\";\n\nimport * as React from \"react\";\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { Minus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ntype InputOTPProps = React.ComponentProps<typeof OTPInput> & {\n  /** Classes for the wrapper around the slots (use `className` for the hidden input). */\n  containerClassName?: string;\n};\n\n/**\n * A one-time-code field. One real input sits under the slots, so paste, autofill (`autoComplete=\"one-time-code\"`),\n * backspace and screen readers all behave like a normal text field.\n */\nfunction InputOTP({ className, containerClassName, ...props }: InputOTPProps) {\n  return (\n    <OTPInput\n      data-slot=\"input-otp\"\n      autoComplete=\"one-time-code\"\n      inputMode=\"numeric\"\n      containerClassName={cn(\n        \"group/otp flex items-center gap-2 has-disabled:opacity-50\",\n        containerClassName,\n      )}\n      className={cn(\"disabled:cursor-not-allowed\", className)}\n      {...props}\n    />\n  );\n}\n\ntype InputOTPGroupProps = React.ComponentProps<\"div\">;\nfunction InputOTPGroup({ className, ...props }: InputOTPGroupProps) {\n  return (\n    <div data-slot=\"input-otp-group\" className={cn(\"flex items-center\", className)} {...props} />\n  );\n}\n\ntype InputOTPSlotProps = React.ComponentProps<\"div\"> & {\n  /** Position of this slot in the code, starting at 0. */\n  index: number;\n  /** Mark the slot invalid (destructive border) without changing the field. */\n  invalid?: boolean;\n};\nfunction InputOTPSlot({ index, className, invalid, ...props }: InputOTPSlotProps) {\n  const context = React.useContext(OTPInputContext);\n  const { char, hasFakeCaret, isActive } = context?.slots[index] ?? {};\n  return (\n    <div\n      data-slot=\"input-otp-slot\"\n      data-active={isActive}\n      data-filled={char ? \"true\" : undefined}\n      data-invalid={invalid || undefined}\n      className={cn(\n        \"relative flex h-11 w-10 items-center justify-center border-y border-e border-input bg-background text-base font-medium tabular-nums shadow-xs transition-[border-color,box-shadow] first:rounded-s-md first:border-s last:rounded-e-md dark:bg-input/30\",\n        \"data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[3px] data-[active=true]:ring-ring/50\",\n        \"data-[invalid=true]:border-destructive data-[invalid=true]:data-[active=true]:ring-destructive/30!\",\n        \"group-has-[[aria-invalid=true]]/otp:border-destructive group-has-[[aria-invalid=true]]/otp:data-[active=true]:ring-destructive/30!\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        key={char ?? \"empty\"}\n        className={cn(char && \"animate-in zoom-in-75 fade-in-0 duration-150 motion-reduce:animate-none\")}\n      >\n        {char}\n      </span>\n      {hasFakeCaret && (\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n          <span className=\"h-5 w-px animate-pulse bg-foreground motion-reduce:animate-none\" />\n        </span>\n      )}\n    </div>\n  );\n}\n\ntype InputOTPSeparatorProps = React.ComponentProps<\"div\">;\nfunction InputOTPSeparator(props: InputOTPSeparatorProps) {\n  return (\n    <div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n      <Minus aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n    </div>\n  );\n}\n\nexport {\n  InputOTP,\n  InputOTPGroup,\n  InputOTPSlot,\n  InputOTPSeparator,\n  type InputOTPProps,\n  type InputOTPGroupProps,\n  type InputOTPSlotProps,\n  type InputOTPSeparatorProps,\n};\n",
      "type": "registry:ui",
      "target": "@components/ballmac/input-otp.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "otp",
      "code",
      "verification",
      "input"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "<InputOTP maxLength={6}> with InputOTPGroup, InputOTPSlot index and InputOTPSeparator. onComplete fires when every slot is filled.",
      "whenToUse": [
        "Two-step sign-in and email verification",
        "PIN entry"
      ],
      "whenNotToUse": [
        "Free-form text codes; use input",
        "Long passwords; use password-input"
      ],
      "composesWith": [
        "field",
        "label"
      ],
      "a11y": [
        {
          "keys": "Type / paste",
          "action": "Fills slots; autofill works with one-time-code"
        },
        {
          "keys": "Backspace / arrows",
          "action": "Edit like a normal text field"
        },
        {
          "keys": "Screen readers",
          "action": "One text input is announced, not six boxes"
        }
      ],
      "customization": [
        "maxLength and pattern",
        "groups and separators",
        "aria-invalid",
        "onComplete"
      ]
    },
    "source": {
      "name": "shadcn/ui Input OTP",
      "url": "https://github.com/shadcn-ui/ui",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 shadcn",
      "modified": true
    },
    "examples": [
      "input-otp-demo",
      "input-otp-states"
    ],
    "url": "https://ui.ballmac.com/components/input-otp"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}