import { useRef, useState } from 'react'; import { View, Text, PanResponder, Animated, StyleSheet } from 'react-native'; const TRACK_HEIGHT = 58; const HANDLE_SIZE = 46; const PADDING = 6; const THRESHOLD = 0.85; // 85% of track = confirmed interface Props { label?: string; color?: string; onConfirm: () => void; disabled?: boolean; } export function SlideToConfirm({ label = 'Slide to confirm', color = '#059669', onConfirm, disabled = false }: Props) { const pan = useRef(new Animated.Value(0)).current; const [done, setDone] = useState(false); const [trackW, setTrackW] = useState(0); const maxX = trackW - HANDLE_SIZE - PADDING * 2; const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => !disabled && !done, onStartShouldSetPanResponderCapture: () => !disabled && !done, onMoveShouldSetPanResponder: () => !disabled && !done, onMoveShouldSetPanResponderCapture: () => !disabled && !done, onPanResponderMove: (_, gs) => { const x = Math.max(0, Math.min(gs.dx, maxX)); pan.setValue(x); }, onPanResponderRelease: (_, gs) => { const x = Math.max(0, Math.min(gs.dx, maxX)); if (maxX > 0 && x / maxX >= THRESHOLD) { // Snap to end + confirm Animated.timing(pan, { toValue: maxX, duration: 120, useNativeDriver: false }).start(() => { setDone(true); onConfirm(); }); } else { // Snap back Animated.spring(pan, { toValue: 0, useNativeDriver: false, speed: 20 }).start(); } }, }) ).current; // Interpolate opacity of the label as handle moves right // Guard: inputRange must be monotonically non-decreasing (maxX could be 0 before layout) const safeMax = Math.max(maxX * 0.5, 1); const labelOpacity = pan.interpolate({ inputRange: [0, safeMax], outputRange: [1, 0], extrapolate: 'clamp', }); return ( setTrackW(e.nativeEvent.layout.width)} style={[styles.track, { backgroundColor: done ? color : '#F1F5F9', borderColor: done ? color : '#E2E8F0' }]} > {/* Label */} {done ? '✓ Confirmed!' : label} {/* Handle */} {!done && ( {'›'} )} ); } const styles = StyleSheet.create({ track: { height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2, borderWidth: 1.5, justifyContent: 'center', alignItems: 'center', overflow: 'hidden', position: 'relative', }, label: { fontSize: 15, fontWeight: '700', letterSpacing: 0.3, }, handle: { position: 'absolute', top: PADDING, width: HANDLE_SIZE, height: HANDLE_SIZE, borderRadius: HANDLE_SIZE / 2, alignItems: 'center', justifyContent: 'center', elevation: 3, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, }, });