{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "fab", "type": "registry:ui", "title": "FAB", "description": "Floating action button — sm/md/extended.", "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": [ "tailwind-variants" ], "files": [ { "path": "registry/shared/ui/fab.tsx", "type": "registry:ui", "target": "components/ui/fab.tsx", "content": "import { useState, type ReactNode } from 'react';\nimport {\n Platform,\n Pressable,\n StyleSheet,\n View,\n type GestureResponderEvent,\n type PressableProps,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { useSafeAreaInsets } from 'react-native-safe-area-context';\nimport { Text } from '@/components/ui/text';\nimport { cn, useThemeColor } from '@/lib/utils';\n\nexport type FabSize = 'sm' | 'md' | 'lg' | 'extended';\nexport type FabVariant = 'default' | 'secondary' | 'destructive' | 'outline';\n\nexport interface FabProps extends Omit {\n /** Icon element (e.g. Plus, Camera, Send). */\n icon?: ReactNode;\n /** Label for Extended FAB. */\n label?: string;\n size?: FabSize;\n variant?: FabVariant;\n /** Automatically floats at bottom-right of screen over content. Default: false. */\n floating?: boolean;\n /** Auto-offset bottom padding for device Home Indicator when floating. Default: true. */\n safeArea?: boolean;\n className?: string;\n}\n\nconst SIZES = {\n sm: { size: 40, iconSize: 18, padHorizontal: 14 },\n md: { size: 56, iconSize: 24, padHorizontal: 20 },\n lg: { size: 64, iconSize: 28, padHorizontal: 24 },\n} as const;\n\n/**\n * Floating Action Button (FAB) engineered for primary screen actions.\n * Features solid native background colors, elevation shadows, extended pill mode,\n * and reliable viewport-relative floating placement.\n */\nexport function Fab({\n icon,\n label,\n size = 'md',\n variant = 'default',\n floating = false,\n safeArea = true,\n className,\n disabled = false,\n style,\n onPressIn,\n onPressOut,\n ...props\n}: FabProps) {\n const insets = useSafeAreaInsets();\n const colors = useThemeColor();\n const [isPressed, setIsPressed] = useState(false);\n\n const isExtended = Boolean(label);\n const sizeConfig = size === 'extended' || isExtended ? SIZES.md : SIZES[size];\n\n const getVariantColors = (): { bg: string; fg: string; border?: string } => {\n switch (variant) {\n case 'secondary':\n return { bg: colors.muted, fg: colors.foreground };\n case 'destructive':\n return { bg: colors.destructive, fg: '#ffffff' };\n case 'outline':\n return {\n bg: colors.card,\n fg: colors.foreground,\n border: colors.border,\n };\n case 'default':\n default:\n return { bg: colors.primary, fg: colors.primaryForeground };\n }\n };\n\n const themeColors = getVariantColors();\n\n const dynamicStyle: ViewStyle = {\n backgroundColor: themeColors.bg,\n borderColor: themeColors.border ?? 'transparent',\n borderWidth: themeColors.border ? 1 : 0,\n opacity: disabled ? 0.5 : isPressed ? 0.85 : 1,\n transform: [{ scale: isPressed && !disabled ? 0.94 : 1 }],\n };\n\n const buttonContent = (\n {\n setIsPressed(true);\n onPressIn?.(e);\n }}\n onPressOut={(e: GestureResponderEvent) => {\n setIsPressed(false);\n onPressOut?.(e);\n }}\n className={cn(className)}\n style={[\n styles.baseButton,\n isExtended\n ? {\n height: sizeConfig.size,\n borderRadius: sizeConfig.size / 2,\n paddingHorizontal: sizeConfig.padHorizontal,\n }\n : {\n width: sizeConfig.size,\n height: sizeConfig.size,\n borderRadius: sizeConfig.size / 2,\n },\n dynamicStyle,\n styles.elevationShadow,\n style as StyleProp,\n ]}\n {...props}\n >\n \n {icon}\n {label ? (\n \n {label}\n \n ) : null}\n \n \n );\n\n if (floating) {\n return (\n \n {buttonContent}\n \n );\n }\n\n return buttonContent;\n}\n\nconst styles = StyleSheet.create({\n baseButton: {\n alignItems: 'center',\n justifyContent: 'center',\n borderCurve: 'continuous',\n },\n contentRow: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'center',\n gap: 8,\n },\n labelText: {\n fontWeight: '700',\n letterSpacing: -0.2,\n },\n elevationShadow: {\n ...Platform.select({\n ios: {\n shadowColor: '#000000',\n shadowOffset: { width: 0, height: 4 },\n shadowOpacity: 0.22,\n shadowRadius: 8,\n },\n android: {\n elevation: 6,\n },\n default: {},\n }),\n },\n floatingContainer: {\n position: 'absolute',\n right: 20,\n zIndex: 999,\n },\n});\n" } ] }