# Input OTP

Accessible one-time password component with copy-paste functionality.

- Install: `php artisan blatui:add input-otp`
- Source: https://blatui.remix-it.com/r/input-otp.json
- Composer: `composer require mallardduck/blade-lucide-icons gehrisandro/tailwind-merge-laravel`

## resources/views/components/ui/input-otp.blade.php

```blade
@props([
    'name' => null,
    'maxlength' => 6,
    'value' => '',
    'disabled' => false,
    'alphanumeric' => false,
    'ariaLabel' => 'One-time password',
])

@php
    $inputmode = $alphanumeric ? 'text' : 'numeric';
    $pattern = $alphanumeric ? '[a-zA-Z0-9]*' : '[0-9]*';

    // Livewire bridge — entangle Alpine state with a consumer's wire:model when present.
    $wireModel = \Illuminate\View\ComponentAttributeBag::hasMacro('wire') ? $attributes->wire('model') : null;
    $hasWire = $wireModel && is_string($wireModel->value()) && $wireModel->value() !== '';
    if ($hasWire) { $attributes = $attributes->whereDoesntStartWith('wire:model'); }
@endphp

<div
    data-slot="input-otp"
    x-data="{
        value: @if ($hasWire)@entangle($wireModel)@else @js((string) $value)@endif,
        max: {{ $maxlength }},
        focused: false,
        get active() { return this.focused ? Math.min(this.value.length, this.max - 1) : -1 }
    }"
    {{ $attributes->twMerge('relative flex items-center gap-2 has-disabled:opacity-50') }}
>
    <input
        x-ref="input"
        x-model="value"
        :maxlength="max"
        @focus="focused = true"
        @blur="focused = false"
        inputmode="{{ $inputmode }}"
        autocomplete="one-time-code"
        pattern="{{ $pattern }}"
        aria-label="{{ $ariaLabel }}"
        @if ($name) name="{{ $name }}" @endif
        @if ($disabled) disabled @endif
        class="absolute inset-0 z-10 h-full w-full cursor-default opacity-0 disabled:cursor-not-allowed"
    >
    {{ $slot }}
</div>
```

## resources/views/components/ui/input-otp-group.blade.php

```blade
<div data-slot="input-otp-group" {{ $attributes->twMerge('flex items-center') }}>
    {{ $slot }}
</div>
```

## resources/views/components/ui/input-otp-separator.blade.php

```blade
<div data-slot="input-otp-separator" aria-hidden="true" {{ $attributes }}>
    <x-lucide-minus class="size-4" />
</div>
```

## resources/views/components/ui/input-otp-slot.blade.php

```blade
@props(['index' => 0])

<div
    data-slot="input-otp-slot"
    aria-hidden="true"
    @click="$refs.input.focus()"
    :data-active="active === {{ $index }}"
    {{ $attributes->twMerge('data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]') }}
>
    <span x-text="value[{{ $index }}] || ''"></span>
    <div
        x-show="active === {{ $index }} && value.length === {{ $index }}"
        x-cloak
        class="pointer-events-none absolute inset-0 flex items-center justify-center"
    >
        <div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
    </div>
</div>
```
