diff --git a/components/SlideToConfirm.tsx b/components/SlideToConfirm.tsx
index a48d75e..dd668c7 100644
--- a/components/SlideToConfirm.tsx
+++ b/components/SlideToConfirm.tsx
@@ -1,9 +1,12 @@
/**
* SlideToConfirm — pure RN, no RNGH, no Reanimated.
*
- * Key fix: Animated.event pipes gesture data directly to native thread
- * (no JS bridge delay) — this is what makes the handle actually track the finger.
- * useNativeDriver: true on all animations for 60fps smoothness.
+ * Key findings (Conan investigation):
+ * - Animated.event + useNativeDriver:true is BROKEN with PanResponder — gestureState
+ * (dx/dy) is JS-computed so native driver skips setValue() entirely. Never use.
+ * - useNativeDriver: false is fine on New Arch (Fabric+JSI) — no bridge penalty.
+ * - onStartShouldSetPanResponderCapture claims gesture in capture phase BEFORE
+ * the Modal container can contest it on iOS — fixes the delayed-grant lag.
*/
import { useEffect, useRef, useState } from 'react';
import {
@@ -37,18 +40,16 @@ export function SlideToConfirm({
style,
color,
}: Props) {
- // trackWidthRef — read inside PanResponder callbacks (no stale closure)
- // trackWidthState — triggers re-render so opacity interpolation updates
const trackWidthRef = useRef(0);
const [trackWidthState, setTrackWidthState] = useState(0);
- // pan drives all animation — useNativeDriver: true for all animations
+ // useNativeDriver: false — mandatory for PanResponder gestureState (dx/dy are JS-computed)
const pan = useRef(new Animated.Value(0)).current;
const [isConfirmed, setIsConfirmed] = useState(false);
const confirmedRef = useRef(false);
- // Live refs — break stale closures inside PanResponder
+ // Live refs — break stale closures inside PanResponder (created once in useRef)
const disabledRef = useRef(disabled);
disabledRef.current = disabled;
const onConfirmRef = useRef(onConfirm);
@@ -60,7 +61,7 @@ export function SlideToConfirm({
const snapToEnd = () => {
Animated.spring(pan, {
toValue: getMaxX(),
- useNativeDriver: true,
+ useNativeDriver: false,
damping: 18,
stiffness: 220,
}).start(({ finished }) => {
@@ -75,7 +76,7 @@ export function SlideToConfirm({
Animated.timing(pan, {
toValue: 0,
duration: SNAP_DURATION,
- useNativeDriver: true,
+ useNativeDriver: false,
}).start(({ finished }) => {
if (finished && !disabledRef.current) runTip();
});
@@ -84,8 +85,8 @@ export function SlideToConfirm({
const runTip = () => {
if (disabledRef.current) return;
Animated.sequence([
- Animated.timing(pan, { toValue: TIP_DISTANCE, duration: TIP_DURATION, useNativeDriver: true }),
- Animated.timing(pan, { toValue: 0, duration: TIP_DURATION, useNativeDriver: true }),
+ Animated.timing(pan, { toValue: TIP_DISTANCE, duration: TIP_DURATION, useNativeDriver: false }),
+ Animated.timing(pan, { toValue: 0, duration: TIP_DURATION, useNativeDriver: false }),
]).start();
};
@@ -96,10 +97,12 @@ export function SlideToConfirm({
const panResponder = useRef(
PanResponder.create({
- // Claim gesture on touch start — ensures dx=0 at grant, no jump
+ // Capture phase — claims gesture BEFORE Modal container can contest it on iOS
+ // This prevents the delayed-grant that made v1/v2 appear laggy
+ onStartShouldSetPanResponderCapture: () =>
+ !disabledRef.current && trackWidthRef.current > 0,
onStartShouldSetPanResponder: () =>
!disabledRef.current && trackWidthRef.current > 0,
- // Also claim on move (horizontal dominant) as fallback
onMoveShouldSetPanResponder: (_, gs) =>
!disabledRef.current &&
trackWidthRef.current > 0 &&
@@ -108,14 +111,14 @@ export function SlideToConfirm({
onPanResponderGrant: () => {
confirmedRef.current = false;
pan.stopAnimation();
- pan.setValue(0); // reset — Animated.event maps dx from 0
+ pan.setValue(0);
},
- // Animated.event pipes dx directly to native thread — true 1:1 finger tracking
- onPanResponderMove: Animated.event(
- [null, { dx: pan }],
- { useNativeDriver: true }
- ),
+ onPanResponderMove: (_, gs) => {
+ const maxX = getMaxX();
+ const clamped = Math.min(Math.max(gs.dx, 0), maxX);
+ pan.setValue(clamped);
+ },
onPanResponderRelease: (_, gs) => {
const maxX = getMaxX();
@@ -145,7 +148,6 @@ export function SlideToConfirm({
outputRange: [0, 1],
extrapolate: 'clamp',
});
- // Clamp translateX visually so handle doesn't overshoot track
const clampedX = pan.interpolate({
inputRange: [0, maxX],
outputRange: [0, maxX],
@@ -168,7 +170,6 @@ export function SlideToConfirm({
setTrackWidthState(w);
}}
>
- {/* "Slide to confirm" label — fades out as handle moves right */}
{label}
- {/* "Confirmed ✓" label — fades in near end */}
Confirmed ✓
- {/* Sliding handle — driven by Animated.event (native thread) */}