'use client' import { useRef, type KeyboardEvent } from 'react' import { cn } from '../../lib/cn' import { VisuallyHidden } from '../VisuallyHidden' import type { IconComponent } from '../../lib/types' export interface SegmentedOption { value: T label: string icon?: IconComponent /** * Show the icon alone. The label is still required and still announced — an * icon-only control without one is a control nobody can name. */ iconOnly?: boolean disabled?: boolean } export type SegmentedControlSize = 'sm' | 'md' export type SegmentedControlOrientation = 'horizontal' | 'vertical' const SIZES: Record = { sm: 'h-7 px-3 text-[12px]', md: 'h-8 px-3.5 text-[13px]', } export interface SegmentedControlProps { options: SegmentedOption[] value: T onValueChange: (value: T) => void /** Accessible name for the set. */ label: string /** Control height: 28px or 32px. */ size?: SegmentedControlSize /** vertical is the stacked form the mobile drawer uses. */ orientation?: SegmentedControlOrientation /** Drop the lifted track, for use inside an already-filled surface. */ bare?: boolean /** Stretch the control to the container width. */ fullWidth?: boolean /** Merged last, so it wins. */ className?: string } /** * Single-select from a small set of options — the primary nav pill track. * Implemented as a radiogroup with roving focus, so arrow keys move between * options and only the selected one is a tab stop. */ export function SegmentedControl({ options, value, onValueChange, label, size = 'md', orientation = 'horizontal', bare = false, fullWidth = false, className, }: SegmentedControlProps) { const refs = useRef<(HTMLButtonElement | null)[]>([]) const move = (event: KeyboardEvent, index: number) => { const forward = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown' const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp' if (event.key !== forward && event.key !== back) return event.preventDefault() const step = event.key === forward ? 1 : -1 const total = options.length for (let offset = 1; offset <= total; offset += 1) { const next = (index + step * offset + total * offset) % total if (!options[next].disabled) { onValueChange(options[next].value) refs.current[next]?.focus() return } } } return (
{options.map((option, index) => { const selected = option.value === value const Icon = option.icon return ( ) })}
) }