{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "pressable", "type": "registry:ui", "title": "Pressable", "description": "Pressable with feedback variants (opacity/highlight).", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json" ], "dependencies": [ "tailwind-variants" ], "files": [ { "path": "registry/shared/ui/pressable.tsx", "type": "registry:ui", "target": "components/ui/pressable.tsx", "content": "import { useRef, useState, type ReactNode } from 'react';\nimport {\n Animated,\n type GestureResponderEvent,\n Pressable as RNPressable,\n type PressableProps as RNPressableProps,\n type PressableStateCallbackType,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { cn, useThemeColor } from '@/lib/utils';\n\nexport type PressableVariant =\n | 'plain'\n | 'opacity'\n | 'highlight'\n | 'scale'\n | 'bounce';\n\nexport interface PressableProps extends Omit {\n variant?: PressableVariant;\n /** Active opacity when pressed (default: 0.72 for opacity/bounce variant). */\n activeOpacity?: number;\n /** Active scale when pressed (default: 0.96 for scale/bounce variant). */\n activeScale?: number;\n /** Trigger tactile haptic vibration on press (iOS / Android). */\n haptic?: boolean | 'light' | 'medium' | 'heavy' | 'selection';\n className?: string;\n contentStyle?: StyleProp;\n children?: ReactNode | ((state: PressableStateCallbackType) => ReactNode);\n}\n\ninterface HapticsModule {\n impactAsync: (style: number) => Promise;\n selectionAsync: () => Promise;\n ImpactFeedbackStyle?: {\n Light: number;\n Medium: number;\n Heavy: number;\n };\n}\n\nlet Haptics: HapticsModule | null = null;\ntry {\n Haptics = require('expo-haptics') as unknown as HapticsModule;\n} catch {\n // Graceful fallback when expo-haptics is not installed\n}\n\nfunction triggerHaptic(type: PressableProps['haptic']) {\n if (!Haptics || !type) return;\n try {\n if (type === true || type === 'light') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle?.Light ?? 0);\n } else if (type === 'medium') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle?.Medium ?? 1);\n } else if (type === 'heavy') {\n Haptics.impactAsync(Haptics.ImpactFeedbackStyle?.Heavy ?? 2);\n } else if (type === 'selection') {\n Haptics.selectionAsync();\n }\n } catch {\n // Ignore haptic errors on unsupported devices\n }\n}\n\n/**\n * Animated tactile RN Pressable with spring scale, smooth opacity dimming,\n * background highlight, and haptics running directly on the native UI thread.\n */\nexport function Pressable({\n variant = 'plain',\n activeOpacity = 0.72,\n activeScale = 0.96,\n haptic,\n className,\n disabled = false,\n hitSlop = 6,\n style,\n contentStyle,\n onPress,\n onPressIn,\n onPressOut,\n children,\n ...props\n}: PressableProps) {\n const colors = useThemeColor();\n const [isPressed, setIsPressed] = useState(false);\n\n const scaleAnim = useRef(new Animated.Value(1)).current;\n const opacityAnim = useRef(new Animated.Value(1)).current;\n\n const handlePressIn = (e: GestureResponderEvent) => {\n if (disabled) return;\n setIsPressed(true);\n\n if (haptic) {\n triggerHaptic(haptic);\n }\n\n if (variant === 'scale' || variant === 'bounce') {\n Animated.spring(scaleAnim, {\n toValue: activeScale,\n damping: 18,\n stiffness: 350,\n mass: 0.6,\n useNativeDriver: true,\n }).start();\n }\n\n if (variant === 'opacity' || variant === 'bounce') {\n Animated.timing(opacityAnim, {\n toValue: activeOpacity,\n duration: 80,\n useNativeDriver: true,\n }).start();\n }\n\n onPressIn?.(e);\n };\n\n const handlePressOut = (e: GestureResponderEvent) => {\n if (disabled) return;\n setIsPressed(false);\n\n if (variant === 'scale' || variant === 'bounce') {\n Animated.spring(scaleAnim, {\n toValue: 1,\n damping: 18,\n stiffness: 350,\n mass: 0.6,\n useNativeDriver: true,\n }).start();\n }\n\n if (variant === 'opacity' || variant === 'bounce') {\n Animated.timing(opacityAnim, {\n toValue: 1,\n duration: 120,\n useNativeDriver: true,\n }).start();\n }\n\n onPressOut?.(e);\n };\n\n const isAnimated = variant !== 'plain' && variant !== 'highlight';\n\n return (\n \n \n {typeof children === 'function'\n ? children({ pressed: isPressed, hovered: false })\n : children}\n \n \n );\n}\n" } ] }