import { useId, useRef } from 'react' import type { ReactNode, KeyboardEvent } from 'react' export interface SegmentOption { value: T label: ReactNode icon?: ReactNode disabled?: boolean /** Optional `aria-label` override for icon-only options. */ ariaLabel?: string } interface Props { options: ReadonlyArray> value: T onChange: (value: T) => void ariaLabel: string /** Stretch each segment to fill the row equally. Defaults to true. */ stretch?: boolean size?: 'sm' | 'md' className?: string } /** * Radiogroup-style segmented control — exclusively-selected options * rendered in a single rounded row. Replaces hand-rolled "row of buttons, * one is highlighted" patterns scattered across pages. * * Keyboard support: ←/→ to move focus + select, Home/End jump to ends. */ export default function SegmentedControl({ options, value, onChange, ariaLabel, stretch = true, size = 'md', className, }: Props) { const groupId = useId() const refs = useRef>([]) const focusAt = (next: number) => { const enabled = options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0) if (enabled.length === 0) return const wrapped = ((next % enabled.length) + enabled.length) % enabled.length const targetIdx = enabled[wrapped] refs.current[targetIdx]?.focus() onChange(options[targetIdx].value) } const onKey = (e: KeyboardEvent, idx: number) => { const enabled = options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0) const cursor = enabled.indexOf(idx) switch (e.key) { case 'ArrowRight': case 'ArrowDown': e.preventDefault() focusAt(cursor + 1) break case 'ArrowLeft': case 'ArrowUp': e.preventDefault() focusAt(cursor - 1) break case 'Home': e.preventDefault() focusAt(0) break case 'End': e.preventDefault() focusAt(enabled.length - 1) break } } const sizeClass = size === 'sm' ? 'text-xs px-2.5 py-1.5' : 'text-sm px-3 py-2' return (
{options.map((opt, i) => { const active = opt.value === value return ( ) })}
) }