import { useCallback } from 'react'; import { StyleSheet, Text, View, ViewStyle } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; interface Props { onConfirm: () => void; label?: string; color?: string; threshold?: number; disabled?: boolean; style?: ViewStyle; } const TRACK_HEIGHT = 60; const HANDLE_SIZE = 48; const TRACK_PADDING = 5; export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', color = '#059669', threshold = 0.82, disabled = false, style, }: Props) { const translateX = useSharedValue(0); const maxX = useSharedValue(200); // updated on layout const triggerConfirm = useCallback(() => { onConfirm(); }, [onConfirm]); const pan = Gesture.Pan() .enabled(!disabled) .minDistance(0) .activeOffsetX([-5, 5]) .onUpdate((e) => { 'worklet'; translateX.value = Math.max(0, Math.min(e.translationX, maxX.value)); }) .onEnd(() => { 'worklet'; if (translateX.value >= maxX.value * threshold) { translateX.value = withSpring(maxX.value, { damping: 20, stiffness: 200, mass: 0.8 }); runOnJS(triggerConfirm)(); } else { translateX.value = withSpring(0, { damping: 20, stiffness: 200, mass: 0.8 }); } }); const animatedHandle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], })); const animatedFill = useAnimatedStyle(() => ({ width: translateX.value + HANDLE_SIZE + TRACK_PADDING, })); const animatedLabel = useAnimatedStyle(() => ({ opacity: Math.max(0, 1 - (translateX.value / Math.max(maxX.value * 0.5, 1))), })); return ( // GestureHandlerRootView inside Modal is REQUIRED on iOS New Arch // Modal creates a new UIWindow — RNGH's gesture manager must be re-rooted here { const w = e.nativeEvent.layout.width; maxX.value = w - HANDLE_SIZE - TRACK_PADDING * 2; }} > {/* Colored fill that grows behind the handle */} {/* Label fades as handle moves right */} {label} {/* Draggable handle */} ); } const styles = StyleSheet.create({ root: { width: '100%' }, track: { height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2, backgroundColor: '#F1F5F9', borderWidth: 1.5, justifyContent: 'center', alignItems: 'center', overflow: 'hidden', position: 'relative', }, fill: { position: 'absolute', left: 0, top: 0, height: TRACK_HEIGHT, zIndex: 1, }, label: { position: 'absolute', zIndex: 2, fontSize: 14, fontWeight: '700', color: '#64748B', textAlign: 'center', width: '100%', paddingRight: 16, }, handle: { position: 'absolute', zIndex: 3, justifyContent: 'center', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.18, shadowRadius: 4, elevation: 4, }, arrow: { fontSize: 26, color: '#fff', fontWeight: '800', marginLeft: 2 }, });