# Radio

> Single-choice picker with `RadioGroup` compound API and roving tabindex.

- Available in: `@sisyphos-ui/react`, `@sisyphos-ui/vue`, `@sisyphos-ui/angular`
- Docs: https://www.sisyphosui.com/docs/components/radio
- WAI-ARIA pattern: [Radio Group](https://www.w3.org/WAI/ARIA/apg/patterns/radio/)

## Installation

Pick the framework binding that matches your stack:

```bash
pnpm add @sisyphos-ui/react   # React 18+
pnpm add @sisyphos-ui/vue     # Vue 3+
pnpm add @sisyphos-ui/angular # Angular 17+
```

## Import

```tsx
import "@sisyphos-ui/react/styles.css";
import { Radio } from "@sisyphos-ui/react";
```

## Framework usage

### React 18+

```tsx
import { useState } from "react";
import { RadioGroup, Radio } from "@sisyphos-ui/react";

export const Plan = () => {
  const [plan, setPlan] = useState<string | number>("pro");
  return (
    <RadioGroup label="Plan" value={plan} onChange={setPlan} variant="card">
      <Radio value="free" label="Free" />
      <Radio value="pro" label="Pro" />
    </RadioGroup>
  );
};
```

### Vue 3+

```vue
<script setup lang="ts">
import { ref } from "vue";
import { RadioGroup, Radio } from "@sisyphos-ui/vue";
const plan = ref<string | number>("pro");
</script>

<template>
  <RadioGroup label="Plan" v-model:value="plan" variant="card">
    <Radio value="free" label="Free" />
    <Radio value="pro" label="Pro" />
  </RadioGroup>
</template>
```

### Angular 17+

```ts
import { Component, signal } from "@angular/core";
import { RadioGroup, Radio } from "@sisyphos-ui/angular";

@Component({
  selector: "app-plan",
  standalone: true,
  imports: [RadioGroup, Radio],
  template: `
    <sui-radio-group label="Plan" [(value)]="plan" variant="card">
      <sui-radio value="free" label="Free" />
      <sui-radio value="pro" label="Pro" />
    </sui-radio-group>
  `,
})
export class PlanComponent {
  plan = signal<string | number>("pro");
}
```

## Examples

### Default

Group multiple `<Radio>` inside a `<RadioGroup>` for shared state.

```tsx
import { useState } from "react";
import { Radio, RadioGroup } from "@sisyphos-ui/react";

export function Example() {
  const [plan, setPlan] = useState<string | number>("pro");

  return (
    <RadioGroup label="Plan" value={plan} onChange={setPlan}>
      <Radio value="free" label="Free" description="For personal projects" />
      <Radio value="pro"  label="Pro — $12/mo" description="For growing teams" />
      <Radio value="team" label="Team — $29/mo" description="Shared workspaces and roles" />
    </RadioGroup>
  );
}
```

### Card variant

Card variant turns each option into a large, clickable surface.

```tsx
import { useState } from "react";
import { Radio, RadioGroup } from "@sisyphos-ui/react";

export function Example() {
  const [billing, setBilling] = useState<string | number>("monthly");

  return (
    <RadioGroup
      variant="card"
      direction="horizontal"
      label="Billing"
      value={billing}
      onChange={setBilling}
    >
      <Radio value="monthly" label="Monthly" description="$12 / month" />
      <Radio value="yearly"  label="Yearly"  description="$120 / year — save 2 months" />
    </RadioGroup>
  );
}
```

### Sizes

`size` on `RadioGroup` cascades to every `Radio` inside.

```tsx
import { Radio, RadioGroup } from "@sisyphos-ui/react";

export function Example() {
  return (
    <>
      <RadioGroup name="size-sm" size="sm" defaultValue="a" label="Small">
        <Radio value="a" label="Option A" />
        <Radio value="b" label="Option B" />
      </RadioGroup>
      <RadioGroup name="size-md" size="md" defaultValue="a" label="Medium">
        <Radio value="a" label="Option A" />
        <Radio value="b" label="Option B" />
      </RadioGroup>
      <RadioGroup name="size-lg" size="lg" defaultValue="a" label="Large">
        <Radio value="a" label="Option A" />
        <Radio value="b" label="Option B" />
      </RadioGroup>
    </>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | `string \| number` | — | Selected value (controlled). Use `defaultValue` for uncontrolled. |
| defaultValue | `string \| number` | — | Initial selected value (uncontrolled). |
| onChange | `(value: string \| number) => void` | — | Called with the new value when the selection changes. |
| options | `RadioOption[]` | — | Flat option array (`value`, `label`, `description`, `icon`, `disabled`) — alternative to composing `<Radio>` children. |
| label | `string` | — | Group label rendered above the options and used as the group's accessible name. |
| direction | `"horizontal" \| "vertical"` | `"vertical"` | Layout direction for the options. |
| variant | `"standard" \| "card" \| "list"` | `"standard"` | Visual style applied to each option. |
| size | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Control size shared by all options. |
| color | `"primary" \| "success" \| "error" \| "warning" \| "info"` | `"primary"` | Semantic color for the selected state. |
| error | `boolean` | `false` | Marks the group as invalid for styling and ARIA. |
| errorMessage | `string` | — | Message shown below the group when `error` is true. |
| allowAddOption | `boolean` | `false` | Shows a trailing add-option button; pair with `onAddOption`. |

## Keyboard interactions

- **Tab** — Moves focus into the group (onto the checked option, or the first option when none is checked).
- **ArrowDown + ArrowRight** — Moves selection to the next option (native radio behavior).
- **ArrowUp + ArrowLeft** — Moves selection to the previous option (native radio behavior).
- **Space** — Selects the focused option.

## Accessibility notes

- Options are wrapped in a `role="radiogroup"` container labeled by the group `label` via `aria-label`.
- `aria-required` and `aria-invalid` mirror `required` / `error` on the group; the error message is announced with `role="alert"`.
- Each option is a native `<input type="radio">` sharing an auto-generated `name`, so browser and AT semantics come for free.
- Each radio is nested inside its `<label>`, so clicking the label (or card/list row) activates the option.
- The custom radio visual is `aria-hidden`; only the native input is exposed to assistive tech.

<!-- exports: { "Radio": "@sisyphos-ui/react" } -->