import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState, } from 'react' import {type StyleProp, View, type ViewStyle} from 'react-native' import Animated, {Easing, LinearTransition} from 'react-native-reanimated' import {useHaptics} from '#/lib/haptics' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {atoms as a, native, platform, useTheme} from '#/alf' import { Button, type ButtonProps, ButtonText, type ButtonTextProps, } from '../Button' const InternalContext = createContext<{ type: 'tabs' | 'radio' size: 'small' | 'large' selectedValue: string selectedPosition: {width: number; x: number} | null onSelectValue: ( value: string, position: {width: number; x: number} | null, ) => void updatePosition: (position: {width: number; x: number}) => void } | null>(null) /** * Segmented control component. * * @example * ```tsx * * * * One * * * * * Two * * * * ``` */ export function Root({ label, type = 'radio', size = 'large', value, onChange, children, style, accessibilityHint, }: { label: string type: 'tabs' | 'radio' size?: 'small' | 'large' value: T onChange: (value: T) => void children: React.ReactNode style?: StyleProp accessibilityHint?: string }) { const t = useTheme() const [selectedPosition, setSelectedPosition] = useState<{ width: number x: number } | null>(null) const contextValue = useMemo(() => { return { type, size, selectedValue: value, selectedPosition, onSelectValue: ( val: string, position: {width: number; x: number} | null, ) => { onChange(val as T) if (position) setSelectedPosition(position) }, updatePosition: (position: {width: number; x: number}) => { setSelectedPosition(currPos => { if ( currPos && currPos.width === position.width && currPos.x === position.x ) { return currPos } return position }) }, } }, [value, selectedPosition, setSelectedPosition, onChange, type, size]) return ( {selectedPosition !== null && ( )} {children} ) } const InternalItemContext = createContext<{ active: boolean pressed: boolean hovered: boolean focused: boolean } | null>(null) export function Item({ value, style, children, onPress: onPressProp, ...props }: {value: string; children: React.ReactNode} & Omit) { const playHaptic = useHaptics() const [position, setPosition] = useState<{x: number; width: number} | null>( null, ) const ctx = useContext(InternalContext) if (!ctx) throw new Error( 'SegmentedControl.Item must be used within a SegmentedControl.Root', ) const active = ctx.selectedValue === value // update position if change was external, and not due to onPress const needsUpdate = active && position && (ctx.selectedPosition?.x !== position.x || ctx.selectedPosition?.width !== position.width) // can't wait for `useEffectEvent` const update = useNonReactiveCallback(() => { if (position) ctx.updatePosition(position) }) useLayoutEffect(() => { if (needsUpdate) { update() } }, [needsUpdate, update]) const onPress = useCallback( (evt: any) => { playHaptic('Light') ctx.onSelectValue(value, position) onPressProp?.(evt) }, [ctx, value, position, onPressProp, playHaptic], ) return ( { const measuredPosition = { x: evt.nativeEvent.layout.x, width: evt.nativeEvent.layout.width, } if (!ctx.selectedPosition && active) { ctx.onSelectValue(value, measuredPosition) } setPosition(measuredPosition) }}> ) } export function ItemText({style, ...props}: ButtonTextProps) { const t = useTheme() const ctx = useContext(InternalItemContext) if (!ctx) throw new Error( 'SegmentedControl.ItemText must be used within a SegmentedControl.Item', ) return ( ) } function Slider({x, width}: {x: number; width: number}) { const t = useTheme() return ( ) }