# Radio Group

A set of checkable buttons where no more than one can be checked at a time.

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

## resources/views/components/ui/radio-group.blade.php

```blade
@props([
    'name' => null,
    'value' => null,
])

@php
    // Livewire bridge — entangle Alpine state with a consumer's wire:model when present.
    // No-op (and stripped) without Livewire, so the component still works in plain Blade/Alpine.
    $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="radio-group"
    role="radiogroup"
    x-data="{ value: @if ($hasWire)@entangle($wireModel)@else @js($value)@endif, rovingValue: @js($value) }"
    x-init="$nextTick(() => { if (rovingValue === null) { const f = $el.querySelector('[role=radio]:not([disabled])'); rovingValue = f?.getAttribute('data-value') ?? null } })"
    @keydown="if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Home','End'].includes($event.key)) { $blatNav($event, { selector: '[role=radio]', orientation: 'both' }); const v = document.activeElement?.getAttribute('data-value'); if (v != null) { value = v; rovingValue = v; } }"
    {{ $attributes->twMerge('grid gap-3') }}
>
    @if ($name)
        <input type="hidden" name="{{ $name }}" :value="value">
    @endif
    {{ $slot }}
</div>
```

## resources/views/components/ui/radio-group-item.blade.php

```blade
@props([
    'value' => null,
    'id' => null,
    'disabled' => false,
])

<button
    type="button"
    role="radio"
    data-value="{{ $value }}"
    @if ($id) id="{{ $id }}" @endif
    @click="value = @js($value); rovingValue = @js($value)"
    @focus="rovingValue = @js($value)"
    :data-state="value === @js($value) ? 'checked' : 'unchecked'"
    :aria-checked="(value === @js($value)).toString()"
    :tabindex="rovingValue === @js($value) ? 0 : -1"
    @if ($disabled) disabled @endif
    data-slot="radio-group-item"
    {{ $attributes->twMerge('border-input text-primary dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50') }}
>
    <span data-slot="radio-group-indicator" class="relative flex items-center justify-center" x-show="value === @js($value)" x-cloak>
        <x-lucide-circle aria-hidden="true" class="fill-current absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
    </span>
</button>
```
