/** * SlideToConfirm — No RNGH dependency. * Uses Reanimated useSharedValue (JSI-direct, sets on UI thread immediately) * + onTouchStart/Move/End on the handle for gesture capture. */ import { useRef } from 'react'; import { StyleSheet, Text, View, ViewStyle } from 'react-native'; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; interface Props { onConfirm: () => void; label?: string; disabled?: boolean; style?: ViewStyle; } const HANDLE_SIZE = 56; const THRESHOLD = 0.80; // 80% of track export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', disabled = false, style }: Props) { const trackWidth = useRef(0); const startX = useRef(0); const confirmed = useRef(false); const translateX = useSharedValue(0); const handleStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], })); const labelStyle = useAnimatedStyle(() => { const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1); const ratio = translateX.value / maxX; return { opacity: 1 - ratio * 1.5 }; }); const onTouchStart = (e: any) => { if (disabled) return; confirmed.current = false; startX.current = e.nativeEvent.pageX - translateX.value; }; const onTouchMove = (e: any) => { if (disabled) return; const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1); const raw = e.nativeEvent.pageX - startX.current; const clamped = Math.min(Math.max(raw, 0), maxX); translateX.value = clamped; if (!confirmed.current && clamped / maxX >= THRESHOLD) { confirmed.current = true; translateX.value = withSpring(maxX, { damping: 18, stiffness: 220 }); runOnJS(onConfirm)(); } }; const onTouchEnd = () => { if (disabled || confirmed.current) return; translateX.value = withSpring(0, { damping: 18, stiffness: 220 }); }; return ( { trackWidth.current = e.nativeEvent.layout.width; }} > {/* Label fades as handle moves right */} {label} {/* Sliding handle */} ); } const styles = StyleSheet.create({ track: { height: 60, backgroundColor: '#0E7490', borderRadius: 30, justifyContent: 'center', overflow: 'hidden', }, trackDisabled: { backgroundColor: '#94A3B8', }, labelWrap: { alignItems: 'center', justifyContent: 'center', }, label: { color: 'rgba(255,255,255,0.85)', fontSize: 15, fontWeight: '600', letterSpacing: 0.3, }, handle: { width: HANDLE_SIZE, height: HANDLE_SIZE, borderRadius: HANDLE_SIZE / 2, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', marginLeft: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.18, shadowRadius: 4, elevation: 4, }, handleDisabled: { backgroundColor: '#E2E8F0', }, arrow: { fontSize: 28, color: '#0891B2', fontWeight: '800', lineHeight: 32, }, });