import type { ControlSize } from "@charcuterie/tokens"; import type { ReactNode } from "react"; import type { ListboxItem } from "./Listbox.tsx"; export type ListboxOptionProps = { isSelected: boolean; /** Already stepped down for a short window by `usePanelItemSize`. */ itemSize: ControlSize; item: ListboxItem; onSelect: (value: string) => void; /** `RovingFocus.register` — membership of the arrow-key group. */ registerFocus: (value: string) => () => void; /** `SinglePicker.register` — membership of the choice. */ registerSelection: (value: string) => () => void; tabIndex: number; trackElement: (value: string, element: HTMLButtonElement | null) => void; }; /** * Its own file for the same reason as `MenuAction` and `TabTrigger`: * **both registrations are effects**, and an effect cannot run in a * loop. * * The two registrations are separate on purpose, exactly as * `TabTrigger` splits them. A disabled option joins the *choice* — it * can still be the seeded `selectedValue` and still announces * `aria-selected` — and stays out of the *focus* group, so the arrow * keys and type-ahead skip it without `RovingFocus` having to know * what "disabled" means. * * ### `option`, not `menuitem` * * An `option` **is** something — the value being chosen — where a * `menuitem` **does** something. So this carries `aria-selected` * (which a menuitem must not), and a screen reader announces * "listbox, selected, 2 of 4" rather than "menu, 4 items". It is a * `