'use client' /** * SegmentedControl — equal-width options in a pill track with a sliding indicator. * * Purpose * - Shared single-select chrome for short fixed option sets (gender, ticket type, * my-events status filters, and similar). * - Soft: white sliding pill on a muted track. * - Solid: dark sliding pill (e.g. free / paid). * * When NOT to use * - Wrapping chip rows of unequal width → keep dedicated chip groups. * - Scrollable many-item rows → use `AppTabs` with a scrollable tab list. */ import type { ReactNode } from 'react' import { motion, useReducedMotion } from 'framer-motion' import Button from '@/components/formElements/Button' import { cn } from '@/lib/cn' export type SegmentedControlVariant = 'soft' | 'solid' export interface SegmentedControlOption { value: T label: ReactNode icon?: ReactNode /** Always-on classes (e.g. per-option color). Active state still toggles weight. */ className?: string } interface SegmentedControlProps { options: readonly SegmentedControlOption[] /** Active value. When it matches no option, the sliding pill is hidden. */ value: T | null | undefined onChange: (value: T) => void ariaLabel: string /** Unique framer-motion layout id so multiple controls on one page do not clash. */ layoutId: string variant?: SegmentedControlVariant size?: 'sm' | 'md' | 'lg' className?: string } const SIZE_CLASSES = { sm: 'min-h-9 text-[13px]', md: 'min-h-11 text-sm', lg: 'min-h-12 text-base', } as const const SegmentedControl = ({ options, value, onChange, ariaLabel, layoutId, variant = 'soft', size = 'md', className, }: SegmentedControlProps) => { const reduceMotion = useReducedMotion() const isSoft = variant === 'soft' if (!options.length) return null return (
{options.map((option) => { const isActive = option.value === value return (
{isActive ? ( ) : null}
) })}
) } export default SegmentedControl