{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bottom-navigation", "type": "registry:ui", "title": "Bottom Navigation", "description": "Tab bar with icon+label items.", "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/bottom-navigation.tsx", "type": "registry:ui", "target": "components/ui/bottom-navigation.tsx", "content": "import {\n cloneElement,\n isValidElement,\n useEffect,\n useRef,\n type ReactNode,\n} from 'react';\nimport {\n Animated,\n Platform,\n Pressable,\n StyleSheet,\n View,\n type ViewProps,\n} from 'react-native';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\nimport { Badge } from '@/components/ui/badge';\nimport { Text } from '@/components/ui/text';\nimport { cn, useThemeColor } from '@/lib/utils';\n\nexport interface BottomNavigationItem {\n key: string;\n label: string;\n icon?: ReactNode;\n activeIcon?: ReactNode;\n badge?: number | string | boolean;\n disabled?: boolean;\n}\n\nexport type BottomNavigationVariant = 'default' | 'floating';\n\nexport interface BottomNavigationProps extends ViewProps {\n items: BottomNavigationItem[];\n value: string;\n onValueChange?: (key: string) => void;\n variant?: BottomNavigationVariant;\n /** Add bottom padding for the device Home Indicator. Default: true. */\n safeArea?: boolean;\n className?: string;\n}\n\ninterface IconWithColorProps {\n color?: string;\n}\n\nfunction NavItemButton({\n item,\n active,\n tint,\n colors,\n onPress,\n}: {\n item: BottomNavigationItem;\n active: boolean;\n tint: string;\n colors: Record;\n onPress: () => void;\n}) {\n const anim = useRef(new Animated.Value(active ? 1 : 0)).current;\n\n useEffect(() => {\n Animated.spring(anim, {\n toValue: active ? 1 : 0,\n damping: 18,\n stiffness: 300,\n mass: 0.6,\n useNativeDriver: true,\n }).start();\n }, [active, anim]);\n\n const rawIcon = active && item.activeIcon ? item.activeIcon : item.icon;\n\n let iconElement: ReactNode = rawIcon;\n if (isValidElement(rawIcon)) {\n const propsObj: unknown = rawIcon.props;\n const explicitColor =\n propsObj &&\n typeof propsObj === 'object' &&\n 'color' in propsObj &&\n typeof propsObj.color === 'string'\n ? propsObj.color\n : tint;\n iconElement = cloneElement(rawIcon, { color: explicitColor });\n }\n\n const capsuleScale = anim.interpolate({\n inputRange: [0, 1],\n outputRange: [0.75, 1],\n });\n\n const capsuleOpacity = anim.interpolate({\n inputRange: [0, 1],\n outputRange: [0, 1],\n });\n\n const iconScale = anim.interpolate({\n inputRange: [0, 1],\n outputRange: [0.95, 1.05],\n });\n\n return (\n [\n styles.navItem,\n pressed && { opacity: 0.7 },\n ]}\n >\n {/* Animated Capsule Box */}\n \n {/* Animated Background Pill */}\n \n\n {/* Animated Icon */}\n \n {iconElement}\n \n\n {/* Notification Badge */}\n {item.badge !== undefined && item.badge !== false && (\n \n {item.badge === true ? (\n \n ) : (\n \n {item.badge}\n \n )}\n \n )}\n \n\n {/* Label */}\n \n {item.label}\n \n \n );\n}\n\n/**\n * Animated Bottom Navigation Bar with spring capsule highlights, floating island variant,\n * and badge counters.\n */\nexport function BottomNavigation({\n items,\n value,\n onValueChange,\n variant = 'default',\n safeArea = true,\n className,\n style,\n ...props\n}: BottomNavigationProps) {\n const insets = useSafeAreaInsets();\n const colors = useThemeColor();\n\n const isFloating = variant === 'floating';\n\n return (\n \n {items.map((item) => {\n const active = item.key === value;\n const tint = active ? colors.primary : colors.mutedForeground;\n\n return (\n onValueChange?.(item.key)}\n />\n );\n })}\n \n );\n}\n\nconst styles = StyleSheet.create({\n standardBar: {\n width: '100%',\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-around',\n minHeight: 56,\n },\n floatingBar: {\n position: 'absolute',\n left: 16,\n right: 16,\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-around',\n minHeight: 60,\n ...Platform.select({\n ios: {\n shadowColor: '#000000',\n shadowOffset: { width: 0, height: 6 },\n shadowOpacity: 0.14,\n shadowRadius: 14,\n },\n android: {\n elevation: 8,\n },\n default: {},\n }),\n },\n navItem: {\n flex: 1,\n minWidth: 0,\n alignItems: 'center',\n justifyContent: 'center',\n paddingVertical: 4,\n gap: 3,\n },\n iconContainer: {\n position: 'relative',\n width: 52,\n height: 30,\n alignItems: 'center',\n justifyContent: 'center',\n },\n animatedCapsule: {\n position: 'absolute',\n width: 52,\n height: 30,\n borderRadius: 15,\n },\n badgeAnchor: {\n position: 'absolute',\n top: -3,\n right: 4,\n },\n dotBadge: {\n width: 8,\n height: 8,\n borderRadius: 4,\n },\n badgeText: {\n fontSize: 9,\n fontWeight: '800',\n color: '#ffffff',\n lineHeight: 12,\n },\n itemLabel: {\n fontSize: 11,\n textAlign: 'center',\n letterSpacing: -0.2,\n },\n});\n" } ] }