Files
fiberops-mobile/components/SlideToConfirm.tsx

135 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (
<View
style={[styles.track, disabled && styles.trackDisabled, style]}
onLayout={e => { trackWidth.current = e.nativeEvent.layout.width; }}
>
{/* Label fades as handle moves right */}
<Animated.View style={[StyleSheet.absoluteFill, styles.labelWrap, labelStyle]} pointerEvents="none">
<Text style={styles.label}>{label}</Text>
</Animated.View>
{/* Sliding handle */}
<Animated.View
style={[styles.handle, disabled && styles.handleDisabled, handleStyle]}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchEnd}
>
<Text style={styles.arrow}></Text>
</Animated.View>
</View>
);
}
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,
},
});