import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";

import * as RadioGroupStories from "./RadioGroup.stories";

<Meta of={RadioGroupStories} />

# RadioGroup

One choice out of many, each on its own row with a label the size of prose.

`RadioGroup` and [`SegmentedControl`](/docs/components-segmentedcontrol--docs) are the
**same composition** — `SinglePicker` + `RovingFocus`, a `radiogroup` of `radio`s where
selection follows focus — and a different shape. The segmented strip is compact, for a
handful of short options that share a control row; the radio group is form-length, where
each option carries a real sentence and an in-line strip would wrap. Choosing between them
is layout, not behaviour, which is why the keyboard model is identical and lives in the same
two hooks.

<Canvas of={RadioGroupStories.Playground} />

## Props

<Controls />

## `radiogroup`, not a stack of checkboxes

A column of `<input type="checkbox">` is the **independent toggles** pattern: nothing in it
says the options are mutually exclusive, so a screen reader announces each as its own
on/off with no "1 of 4" and no clue that checking one unchecks the rest. A single choice
from a named set is a radio group, and ARIA has a pattern for it — which is the same
correction `SegmentedControl` makes to rip-deck's row of `aria-pressed` buttons.

<Canvas of={RadioGroupStories.AllVariants} />

## Selection follows focus

APG gives a radio group one activation mode: the arrow keys both move focus and check.
There is nothing behind an option here to be expensive — no panel, no request — so unlike
`Tabs` there is no `manual` mode and no prop, because a `manual` radio group is one the
arrow keys appear to do nothing in. Tab enters the group once (the roving-tabindex rule),
the arrows move and check together, Home and End jump to the ends, and the ends wrap.

A disabled option is simply **not registered** with the roving group, so the arrows skip it
without any command having to know what "disabled" means. It stays registered with the
picker — it is still one of the options, and still the one a saved `selectedValue` may point
at.

<Canvas of={RadioGroupStories.AllStates} />

## `itemShape="tile"` — the choice tile

A bordered card carrying a name, a line of help and a selected surface, laid out in a grid
that gains columns with its **container**. It is a prop rather than a third component
because **only the box changes**: the element, the `radio` role, the roving tab stop,
selection-follows-focus and `isReadOnly` are the ones already tested on the row shape. A
tile with its border taken away is a row.

`SegmentedControl` earns its own file by being a different *control* — one connected strip,
no radio affordance, options too short to carry a sentence. A tile is this control with a
border, which is a different reason.

[`ActionTiles`](/docs/components-actions-actiontiles--docs) is the same tile that **acts**
rather than holds a value, and it *does* earn its own file: invert every clause of the
paragraph above — no `radio` role, no roving tabindex, no selection-follows-focus, no
`isReadOnly` — and the box is all that is left in common. The two read it from one
`tileStyles.ts`. Ask what a press does: records a value something below reads, or goes
somewhere and is done.

<Canvas of={RadioGroupStories.Tiles} />

Four apps in the fleet had each hand-painted this shape — ten instances between them — and
**not one carried `aria-checked`**. Every one was a bare `<button>` or `<a>` whose selection
was visible only as colour: correct to an eye, silent to a screen reader, and announced as
neither pressed nor "3 of 6". That is what this prop is for, and it is why the semantics are
not the part an app should be writing.

Eight of the ten had a **second line** under the name, which is also why none of them
reached for `RadioGroup`: a plain-text `label` could not express it. `hint` can, and it
renders on a row as readily as on a tile.

### The selected tile is an edge and a surface, never a fill

The chosen tile takes the accent on its **border** and lifts to `surface-overlay`; the radio
dot inside it is unchanged and is still the thing a screen reader reads. A state told in
colour alone is the one state a monochrome ePaper build cannot show at all — the same reason
`Stepper` refuses to say "done" with a hue.

### A tile may lead with an icon

`icon` is `tile` only — a row already leads with its radio, and a second mark either side of
it has nowhere to sit — and it is `aria-hidden` by construction, because the name it sits
above is inside the same button. The library still ships no icons; apps bring their own.

<Canvas of={RadioGroupStories.TilesWithIcons} />

## The grid takes its columns from the container

`repeat(auto-fill, minmax(min(var(--charcuterie-tile-min-inline-size), 100%), 1fr))`, and
each of those three parts is load-bearing:

- **`auto-fill`, not `auto-fit`.** `auto-fit` collapses the empty tracks and lets the
  survivors share the whole row, so six tiles in a 2560px container become six 420px slabs —
  the full-width-row shape in a new costume. `auto-fill` keeps the empty tracks, so a tile
  stays a tile and the grid simply has room to spare.
- **`min(…, 100%)`.** Without it the 200px floor overflows a container narrower than one
  tile, which is a horizontal scrollbar in the Narrow View.
- **A custom property, not an interpolated class.** Tailwind scans source *text* for
  complete class strings, so `` `grid-cols-[…${n}px…]` `` generates nothing, paints nothing
  and reports nothing. `minTileInlineSize` lands in `--charcuterie-tile-min-inline-size`,
  which one written-out literal reads.

This is **not** `useAdaptiveColumns`, and the difference is which question is being asked.
That hook buys a column with **height**, for an unbounded gallery that is going to scroll. A
radio group is a bounded set of options inside a form section, where the only question is
how many fit across the box it was handed.

<Canvas of={RadioGroupStories.Responsive} />

## A read-only group shows the choice without moving it

`isReadOnly` severs the one link that defines this component — selection follows focus — so
the arrow keys and clicks still travel the options for reading but no longer move the
choice. The group announces `aria-readonly`, and the chosen option shows at full contrast
in the **neutral** intent: a value you may read here and change elsewhere. That is distinct
from a single **disabled** option, which is dimmed with `opacity-60` and dropped from the
focus group but stays selectable via `selectedValue`.

## `selectedValue` is the initial value, not a controlled one

It decides the first render and nothing after — Charcuterie owns the choice from then on,
and `onChange` reports it outward. Members register from an effect, so on the first paint
the value lives in the picker's `pendingValue` and the group paints already showing it
rather than flashing empty for a frame — a flash on a desktop, a visible blank on a kiosk
Pi.

<Canvas of={RadioGroupStories.Interactive} />
