diff --git a/src/components/SegmentedControl.tsx b/src/components/SegmentedControl.tsx index 16d8c8e..3211b1f 100644 --- a/src/components/SegmentedControl.tsx +++ b/src/components/SegmentedControl.tsx @@ -2,8 +2,7 @@ import { useEffect } from 'react'; import { Pressable, StyleSheet, - View, - type LayoutChangeEvent + View } from 'react-native'; import Animated, { interpolateColor, @@ -34,40 +33,15 @@ interface SegmentedControlProps { } /** - * Equal-width segmented control on the TabBar "playhead" pattern: one glass - * track, a thumb that glides to the active segment, labels cross-fading to the - * accent via interpolateColor on Animated.Text. Spring-free per theme/motion. + * Equal-width segmented control with a selection pill anchored inside the + * active segment. The structural tie to `focused` keeps the pill and content + * selection synchronized; labels still cross-fade via Animated.Text. */ export function SegmentedControl({ segments, value, onChange }: SegmentedControlProps) { const styles = useStyles(); - const count = segments.length; - const activeIndex = Math.max( - 0, - segments.findIndex((segment) => segment.key === value), - ); - - const trackWidth = useSharedValue(0); - const position = useSharedValue(activeIndex); - - useEffect(() => { - position.value = withTiming(activeIndex, motion.snap); - }, [activeIndex, position]); - - const thumbStyle = useAnimatedStyle(() => { - const segment = count > 0 ? (trackWidth.value - THUMB_INSET * 2) / count : 0; - return { - width: segment, - transform: [{ translateX: position.value * segment }], - }; - }); - - const onTrackLayout = (e: LayoutChangeEvent) => { - trackWidth.value = e.nativeEvent.layout.width; - }; return ( - - + {segments.map((segment) => ( + {focused ? : null} {label} @@ -139,9 +114,10 @@ const useStyles = createThemedStyles((colors) => ({ }, thumb: { position: 'absolute', - top: THUMB_INSET, - bottom: THUMB_INSET, - left: THUMB_INSET, + top: 0, + right: 0, + bottom: 0, + left: 0, backgroundColor: colors.glassHighlight, borderColor: colors.accent, borderWidth: StyleSheet.hairlineWidth, diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 61505f2..7c0a4d5 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -2,8 +2,7 @@ import { useEffect } from 'react'; import { View, Pressable, - StyleSheet, - type LayoutChangeEvent + StyleSheet } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; @@ -53,32 +52,6 @@ export function TabBar({ items, onPress }: TabBarProps) { const styles = useStyles(); const insets = useSafeAreaInsets(); const tabs = items.filter((item) => TAB_META[item.name]); - const count = tabs.length; - const activeIndex = Math.max( - 0, - tabs.findIndex((item) => item.focused), - ); - - // The "playhead": a single accent bar that glides along the top edge to the - // active tab, travelling in the same direction as the scene transition. - const barWidth = useSharedValue(0); - const position = useSharedValue(activeIndex); - - useEffect(() => { - position.value = withTiming(activeIndex, motion.snap); - }, [activeIndex, position]); - - const indicatorStyle = useAnimatedStyle(() => { - const segment = count > 0 ? barWidth.value / count : 0; - return { - width: segment, - transform: [{ translateX: position.value * segment }], - }; - }); - - const onBarLayout = (e: LayoutChangeEvent) => { - barWidth.value = e.nativeEvent.layout.width; - }; return ( @@ -88,11 +61,7 @@ export function TabBar({ items, onPress }: TabBarProps) { styles.bar, { paddingBottom: insets.bottom, height: layout.tabBarHeight + insets.bottom }, ]} - onLayout={onBarLayout} > - - - {tabs.map((item) => { const meta = TAB_META[item.name]; if (!meta) return null; @@ -169,6 +138,11 @@ function TabButton({ meta, focused, onPress }: TabButtonProps) { accessibilityRole="tab" accessibilityState={{ selected: focused }} > + {focused ? ( + + + + ) : null} @@ -194,6 +168,7 @@ const useStyles = createThemedStyles((colors) => ({ position: 'absolute', top: 0, left: 0, + right: 0, alignItems: 'center', }, indicatorBar: {