// components/ui/tabs.tsx import * as React from 'react'; import { View, Pressable, Animated, LayoutChangeEvent, Platform } from 'react-native'; import { cva } from 'class-variance-authority'; import { cn } from '@/lib/utils'; import { Text } from './text'; const isWeb = Platform.OS === 'web'; const tabsListVariants = cva('relative flex-row items-center', { variants: { variant: { pill: 'bg-muted rounded-lg p-1', underline: 'border-b border-border', }, }, defaultVariants: { variant: 'underline' }, }); const tabsTriggerVariants = cva('flex-1 items-center justify-center', { variants: { variant: { pill: 'px-3 py-1.5 rounded-md z-10', underline: '', }, disabled: { true: 'opacity-50', false: '', }, }, defaultVariants: { variant: 'underline', disabled: false }, }); const triggerTextVariants = cva('', { variants: { active: { true: 'text-foreground', false: 'text-muted-foreground', }, }, defaultVariants: { active: false }, }); interface TabLayout { x: number; width: number; height: number } interface TabsContextValue { value: string; onValueChange: (value: string) => void; variant?: 'pill' | 'underline'; } interface TabsListContextValue extends TabsContextValue { registerTab: (tabValue: string, layout: TabLayout) => void; tabLayouts: Map; } const TabsContext = React.createContext(null); const TabsListContext = React.createContext(null); const useTabsContext = () => { const ctx = React.useContext(TabsContext); if (!ctx) throw new Error('Must be used within '); return ctx; }; const useTabsListContext = () => { const ctx = React.useContext(TabsListContext); if (!ctx) throw new Error('Must be used within '); return ctx; }; export interface TabsProps { value: string; onValueChange: (value: string) => void; children: React.ReactNode; variant?: 'pill' | 'underline'; className?: string; } export interface TabsListProps { children: React.ReactNode; className?: string; } export interface TabsTriggerProps { value: string; children: string; className?: string; disabled?: boolean; } export interface TabsContentProps { value: string; children: React.ReactNode; className?: string; } function PillIndicator({ layout }: { layout: TabLayout }) { if (isWeb) { return ( ); } return ; } function NativePillIndicator({ layout }: { layout: TabLayout }) { const x = React.useRef(new Animated.Value(layout.x)).current; const width = React.useRef(new Animated.Value(layout.width)).current; const height = React.useRef(new Animated.Value(layout.height)).current; const isFirst = React.useRef(true); React.useEffect(() => { if (isFirst.current) { x.setValue(layout.x); width.setValue(layout.width); height.setValue(layout.height); isFirst.current = false; return; } Animated.parallel([ Animated.timing(x, { toValue: layout.x, duration: 200, useNativeDriver: false }), Animated.timing(width, { toValue: layout.width, duration: 200, useNativeDriver: false }), Animated.timing(height, { toValue: layout.height, duration: 200, useNativeDriver: false }), ]).start(); }, [layout.x, layout.width, layout.height]); return ( ); } function UnderlineIndicator({ isActive }: { isActive: boolean }) { if (isWeb) { return ( ); } return ; } function NativeUnderlineIndicator({ isActive }: { isActive: boolean }) { const opacity = React.useRef(new Animated.Value(isActive ? 1 : 0)).current; React.useEffect(() => { // @ts-ignore Animated.timing(opacity, { toValue: isActive ? 1 : 0, duration: 200, }).start(); }, [isActive]); return ( ); } export function Tabs({ value, onValueChange, children, variant = 'underline', className }: TabsProps) { return ( {children} ); } export function TabsList({ children, className }: TabsListProps) { const { value, onValueChange, variant } = useTabsContext(); const [tabLayouts, setTabLayouts] = React.useState>(new Map()); const registerTab = React.useCallback((tabValue: string, layout: TabLayout) => { setTabLayouts((prev) => { const next = new Map(prev); next.set(tabValue, layout); return next; }); }, []); const activeLayout = tabLayouts.get(value); return ( {variant === 'pill' && activeLayout && ( )} {children} ); } export function TabsTrigger({ value: triggerValue, children, className, disabled = false }: TabsTriggerProps) { const { value, onValueChange, variant, registerTab } = useTabsListContext(); const isActive = value === triggerValue; const handleLayout = (e: LayoutChangeEvent) => { const { x, width, height } = e.nativeEvent.layout; registerTab(triggerValue, { x, width, height }); }; return ( !disabled && onValueChange(triggerValue)} onLayout={handleLayout} disabled={disabled} className={cn(tabsTriggerVariants({ variant, disabled }), className)} style={isWeb ? { cursor: disabled ? 'not-allowed' : 'pointer' } as any : undefined} > {children} {variant === 'underline' && ( )} ); } export function TabsContent({ value: contentValue, children, className }: TabsContentProps) { const { value } = useTabsContext(); if (value !== contentValue) return null; return {children}; }