{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "app-bar", "type": "registry:ui", "title": "App Bar", "description": "Top navigation bar with leading/back, title, and trailing actions.", "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" ], "dependencies": [ "lucide-react-native" ], "files": [ { "path": "registry/shared/ui/app-bar.tsx", "type": "registry:ui", "target": "components/ui/app-bar.tsx", "content": "import {\n isValidElement,\n type ReactNode,\n} from 'react';\nimport {\n Platform,\n Pressable,\n StyleSheet,\n TextInput,\n View,\n type ViewProps,\n} from 'react-native';\nimport { ChevronLeft, Search, X } from 'lucide-react-native';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\nimport { Badge } from '@/components/ui/badge';\nimport { Text, type TextProps } from '@/components/ui/text';\nimport { cn, useIconColor, useThemeColor } from '@/lib/utils';\n\nexport type AppBarVariant =\n | 'default'\n | 'large'\n | 'search'\n | 'floating'\n | 'glass'\n | 'transparent';\n\nexport interface AppBarProps extends ViewProps {\n /** Leading icon / custom element (e.g. back button, avatar). Overrides `onBack`. */\n leading?: ReactNode;\n /** Back button callback. Renders a rounded chevron well when passed. */\n onBack?: () => void;\n /** Trailing action buttons (search, more, settings). */\n trailing?: ReactNode;\n /** Header visual variant. Default: 'default'. */\n variant?: AppBarVariant;\n /** Main header title. */\n title?: string;\n /** Subtitle text displayed beneath main title. */\n subtitle?: string;\n /** Alignment of title in standard mode. Default: 'center'. */\n alignTitle?: 'center' | 'left';\n /** Value for embedded search input when `variant=\"search\"`. */\n searchValue?: string;\n onSearchChange?: (text: string) => void;\n searchPlaceholder?: string;\n /** Auto-apply safe area top inset padding. Default: true. */\n safeArea?: boolean;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * Top-tier mobile App Bar engineered to iOS 18 Human Interface Guidelines.\n * Features 54px navigation height, symmetric side touch anchors, perfectly anchored\n * badges, non-clipped large display titles, and subtle Apple elevation.\n */\nexport function AppBar({\n leading,\n onBack,\n trailing,\n variant = 'default',\n title,\n subtitle,\n alignTitle = 'center',\n searchValue,\n onSearchChange,\n searchPlaceholder = 'Search...',\n safeArea = true,\n className,\n style,\n children,\n ...props\n}: AppBarProps) {\n const insets = useSafeAreaInsets();\n const iconColor = useIconColor('foreground');\n const colors = useThemeColor();\n\n const isLarge = variant === 'large';\n const isSearch = variant === 'search';\n const isFloating = variant === 'floating';\n const isGlass = variant === 'glass';\n const isTransparent = variant === 'transparent';\n\n const backBtn = onBack ? (\n [\n styles.actionWell,\n {\n backgroundColor: pressed ? colors.accent : 'transparent',\n transform: [{ scale: pressed ? 0.92 : 1 }],\n },\n ]}\n >\n \n \n ) : null;\n\n const resolvedLeading = leading ?? backBtn;\n\n return (\n \n {/* Top Navigation Row (54px height) */}\n \n {/* Leading Side Anchor (Min 44x44px for iOS touch target) */}\n {resolvedLeading}\n\n {/* Center Section: Search Bar OR Standard Title */}\n {isSearch ? (\n \n \n \n \n {searchValue ? (\n onSearchChange?.('')}\n hitSlop={6}\n style={styles.clearSearchBtn}\n >\n \n \n ) : null}\n \n \n ) : !isLarge ? (\n \n {title ? (\n \n \n {title}\n \n {subtitle ? (\n \n {subtitle}\n \n ) : null}\n \n ) : (\n children\n )}\n \n ) : (\n {!title && children}\n )}\n\n {/* Trailing Side Anchor (Min 44x44px) */}\n \n {trailing}\n \n \n\n {/* Large Title Section (iOS Large Display Heading pattern) */}\n {isLarge && (\n \n {title ? (\n \n \n {title}\n \n {subtitle ? (\n \n {subtitle}\n \n ) : null}\n \n ) : (\n children\n )}\n \n )}\n \n );\n}\n\nexport function AppBarTitle({ className, style, ...props }: TextProps) {\n return (\n \n );\n}\n\nexport function AppBarSubtitle({ className, style, ...props }: TextProps) {\n return (\n \n );\n}\n\nexport interface AppBarActionProps {\n className?: string;\n children?: ReactNode;\n badge?: number | string | boolean;\n onPress?: () => void;\n accessibilityLabel?: string;\n}\n\nexport function AppBarAction({\n className,\n children,\n badge,\n onPress,\n accessibilityLabel,\n}: AppBarActionProps) {\n const colors = useThemeColor();\n\n return (\n [\n styles.actionWell,\n {\n backgroundColor: pressed ? colors.accent : 'transparent',\n transform: [{ scale: pressed ? 0.92 : 1 }],\n },\n ]}\n className={className}\n >\n {/* Icon Wrapper with Corner-Anchored Badge */}\n \n {children}\n\n {badge !== undefined && badge !== false && (\n \n {badge === true ? (\n \n ) : (\n \n {badge}\n \n )}\n \n )}\n \n \n );\n}\n\nconst styles = StyleSheet.create({\n topRow: {\n height: 52,\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n width: '100%',\n },\n sideSlot: {\n minWidth: 44,\n height: 44,\n flexDirection: 'row',\n alignItems: 'center',\n },\n trailingSlot: {\n justifyContent: 'flex-end',\n gap: 4,\n },\n centerSlot: {\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n paddingHorizontal: 8,\n },\n leftAlignSlot: {\n alignItems: 'flex-start',\n paddingHorizontal: 2,\n },\n titleStack: {\n alignItems: 'center',\n justifyContent: 'center',\n },\n standardTitle: {\n fontSize: 17,\n fontWeight: '700',\n letterSpacing: -0.4,\n },\n standardSub: {\n fontSize: 11,\n marginTop: 1,\n },\n actionWell: {\n width: 40,\n height: 40,\n borderRadius: 20,\n alignItems: 'center',\n justifyContent: 'center',\n },\n iconWrapper: {\n position: 'relative',\n alignItems: 'center',\n justifyContent: 'center',\n },\n badgeAnchor: {\n position: 'absolute',\n top: -5,\n right: -8,\n zIndex: 10,\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 searchBarWrapper: {\n flex: 1,\n paddingHorizontal: 6,\n },\n searchBarInner: {\n flexDirection: 'row',\n alignItems: 'center',\n height: 40,\n borderRadius: 12,\n borderWidth: StyleSheet.hairlineWidth,\n paddingHorizontal: 10,\n gap: 8,\n },\n searchInputText: {\n flex: 1,\n fontSize: 14,\n paddingVertical: 0,\n height: '100%',\n },\n clearSearchBtn: {\n padding: 2,\n },\n largeTitleSection: {\n paddingHorizontal: 2,\n paddingTop: 12,\n paddingBottom: 4,\n },\n largeTitleStack: {\n gap: 3,\n },\n largeTitleText: {\n fontSize: 28,\n fontWeight: '800',\n letterSpacing: -0.6,\n lineHeight: 34,\n paddingBottom: 2,\n },\n largeSubText: {\n fontSize: 13,\n lineHeight: 18,\n },\n floatingCard: {\n borderWidth: StyleSheet.hairlineWidth,\n borderRadius: 20,\n ...Platform.select({\n ios: {\n shadowColor: '#000000',\n shadowOffset: { width: 0, height: 2 },\n shadowOpacity: 0.05,\n shadowRadius: 8,\n },\n android: {\n elevation: 2,\n },\n default: {},\n }),\n },\n});\n" } ] }