{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tabs", "type": "registry:ui", "title": "Tabs", "description": "Tab list/trigger/content with context.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/text.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "files": [ { "path": "registry/shared/ui/tabs.tsx", "type": "registry:ui", "target": "components/ui/tabs.tsx", "content": "import {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n Animated,\n Platform,\n Pressable,\n StyleSheet,\n View,\n type LayoutRectangle,\n type PressableProps,\n type ViewProps,\n} from 'react-native';\nimport { Text } from '@/components/ui/text';\nimport { cn, useThemeColor } from '@/lib/utils';\n\ninterface TabsContextValue {\n value: string;\n onValueChange: (v: string) => void;\n registerTrigger?: (val: string, layout: LayoutRectangle) => void;\n}\n\nconst TabsContext = createContext({\n value: '',\n onValueChange: () => {},\n});\n\nexport interface TabsProps extends ViewProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n className?: string;\n}\n\nexport function Tabs({\n value: controlled,\n defaultValue = '',\n onValueChange,\n className,\n children,\n ...props\n}: TabsProps) {\n const [uncontrolled, setUncontrolled] = useState(defaultValue);\n const value = controlled ?? uncontrolled;\n\n const [triggerLayouts, setTriggerLayouts] = useState<\n Record\n >({});\n\n const setValue = (v: string) => {\n if (controlled === undefined) {\n setUncontrolled(v);\n }\n onValueChange?.(v);\n };\n\n const registerTrigger = (val: string, layout: LayoutRectangle) => {\n setTriggerLayouts((prev) => {\n if (\n prev[val] &&\n prev[val].x === layout.x &&\n prev[val].width === layout.width\n ) {\n return prev;\n }\n return { ...prev, [val]: layout };\n });\n };\n\n return (\n \n \n {children}\n \n \n );\n}\n\nexport function TabsList({\n className,\n style,\n children,\n ...props\n}: ViewProps & { className?: string }) {\n const { value } = useContext(TabsContext);\n const colors = useThemeColor();\n\n const [layouts, setLayouts] = useState>({});\n\n const translateX = useRef(new Animated.Value(0)).current;\n const widthAnim = useRef(new Animated.Value(0)).current;\n const opacityAnim = useRef(new Animated.Value(0)).current;\n\n const registerTrigger = (val: string, layout: LayoutRectangle) => {\n setLayouts((prev) => {\n if (\n prev[val] &&\n prev[val].x === layout.x &&\n prev[val].width === layout.width\n ) {\n return prev;\n }\n return { ...prev, [val]: layout };\n });\n };\n\n useEffect(() => {\n const activeLayout = layouts[value];\n if (activeLayout && activeLayout.width > 0) {\n Animated.parallel([\n Animated.spring(translateX, {\n toValue: activeLayout.x,\n damping: 24,\n stiffness: 280,\n mass: 0.8,\n useNativeDriver: false,\n }),\n Animated.spring(widthAnim, {\n toValue: activeLayout.width,\n damping: 24,\n stiffness: 280,\n mass: 0.8,\n useNativeDriver: false,\n }),\n Animated.timing(opacityAnim, {\n toValue: 1,\n duration: 120,\n useNativeDriver: false,\n }),\n ]).start();\n }\n }, [value, layouts, translateX, widthAnim, opacityAnim]);\n\n return (\n \n \n {/* Animated Shared Sliding Indicator Pill */}\n \n {children}\n \n \n );\n}\n\nexport interface TabsTriggerProps extends Omit {\n value: string;\n className?: string;\n children?: string;\n}\n\nexport function TabsTrigger({\n value,\n className,\n children,\n disabled,\n style,\n onLayout,\n ...props\n}: TabsTriggerProps) {\n const { value: active, onValueChange, registerTrigger } =\n useContext(TabsContext);\n const colors = useThemeColor();\n const isActive = active === value;\n\n return (\n onValueChange(value)}\n onLayout={(e) => {\n registerTrigger?.(value, e.nativeEvent.layout);\n onLayout?.(e);\n }}\n className={cn(\n 'relative flex-1 items-center justify-center rounded-lg px-3.5 py-2 z-10',\n disabled && 'opacity-50',\n className\n )}\n style={style}\n {...props}\n >\n \n {children}\n \n \n );\n}\n\nexport interface TabsContentProps extends ViewProps {\n value: string;\n className?: string;\n children?: ReactNode;\n}\n\nexport function TabsContent({\n value,\n className,\n children,\n style,\n ...props\n}: TabsContentProps) {\n const { value: active } = useContext(TabsContext);\n const isSelected = active === value;\n\n const fadeAnim = useRef(new Animated.Value(0)).current;\n const translateY = useRef(new Animated.Value(4)).current;\n\n useEffect(() => {\n if (isSelected) {\n fadeAnim.setValue(0);\n translateY.setValue(4);\n Animated.parallel([\n Animated.timing(fadeAnim, {\n toValue: 1,\n duration: 180,\n useNativeDriver: true,\n }),\n Animated.spring(translateY, {\n toValue: 0,\n damping: 22,\n stiffness: 300,\n mass: 0.7,\n useNativeDriver: true,\n }),\n ]).start();\n }\n }, [isSelected, fadeAnim, translateY]);\n\n if (!isSelected) return null;\n\n return (\n \n {children}\n \n );\n}\n\nconst styles = StyleSheet.create({\n indicator: {\n position: 'absolute',\n top: 4,\n bottom: 4,\n borderRadius: 8,\n borderWidth: StyleSheet.hairlineWidth,\n ...Platform.select({\n ios: {\n shadowColor: '#000000',\n shadowOffset: { width: 0, height: 1 },\n shadowOpacity: 0.08,\n shadowRadius: 2,\n },\n android: {\n elevation: 1.5,\n },\n default: {},\n }),\n },\n labelText: {\n fontSize: 13,\n textAlign: 'center',\n },\n});\n" } ] }