import React, { useEffect, useRef, useState } from "react"; import { Animated, LayoutChangeEvent, Pressable, ScrollView, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle, } from "react-native"; export interface TabItem { key: T; label: string; disabled?: boolean; } interface TabsProps { items: TabItem[]; value: T; onChange: (key: T) => void; variant?: "underline" | "pills" | "segmented"; scrollable?: boolean; activeColor?: string; inactiveColor?: string; backgroundColor?: string; pillBackground?: string; style?: StyleProp; labelStyle?: StyleProp; accessibilityLabel?: string; } function Tabs({ items, value, onChange, variant = "underline", scrollable = false, activeColor = "#2563EB", inactiveColor = "#6B7280", backgroundColor = "transparent", pillBackground = "#F3F4F6", style, labelStyle, accessibilityLabel, }: TabsProps) { const layouts = useRef>({}).current; const [, force] = useState(0); const indicatorX = useRef(new Animated.Value(0)).current; const indicatorW = useRef(new Animated.Value(0)).current; useEffect(() => { const target = layouts[value]; if (!target) return; Animated.parallel([ Animated.timing(indicatorX, { toValue: target.x, duration: 200, useNativeDriver: false, }), Animated.timing(indicatorW, { toValue: target.width, duration: 200, useNativeDriver: false, }), ]).start(); }, [value, indicatorX, indicatorW, layouts]); const onTabLayout = (key: string) => (e: LayoutChangeEvent) => { layouts[key] = { x: e.nativeEvent.layout.x, width: e.nativeEvent.layout.width }; if (key === value) { indicatorX.setValue(layouts[key].x); indicatorW.setValue(layouts[key].width); } force((n) => n + 1); }; const Container: any = scrollable ? ScrollView : View; const containerProps = scrollable ? { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: { paddingHorizontal: 4 }, } : {}; const isSegmented = variant === "segmented"; const isPills = variant === "pills"; return ( {items.map((item) => { const isActive = item.key === value; const tone = isActive ? activeColor : inactiveColor; return ( !item.disabled && onChange(item.key)} onLayout={onTabLayout(item.key)} disabled={item.disabled} accessibilityRole="tab" accessibilityState={{ selected: isActive, disabled: !!item.disabled }} accessibilityLabel={item.label} hitSlop={6} style={[ styles.tab, isPills && { backgroundColor: isActive ? activeColor : "transparent", borderRadius: 999, }, isSegmented && { backgroundColor: isActive ? "#FFFFFF" : "transparent", borderRadius: 6, shadowColor: "#000", shadowOffset: { width: 0, height: isActive ? 1 : 0 }, shadowOpacity: isActive ? 0.08 : 0, shadowRadius: 2, elevation: isActive ? 1 : 0, }, { opacity: item.disabled ? 0.5 : 1 }, ]} > {item.label} ); })} {variant === "underline" && ( )} ); } const styles = StyleSheet.create({ outer: { width: "100%", }, row: { flexDirection: "row", alignItems: "center", }, tab: { paddingVertical: 10, paddingHorizontal: 14, alignItems: "center", justifyContent: "center", }, label: { fontSize: 14, }, indicator: { position: "absolute", bottom: 0, height: 2, borderRadius: 2, }, }); export default Tabs;