1diff --git a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js
2index 4ab38424d1f5fc8b2686dd4a0a8058c454a9874d..0c5e0eaa995af50e5f8c02909dc6825121cc61af 100644
3--- a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js
4+++ b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js
5@@ -31,11 +31,13 @@ const Swipeable = props => {
6 enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE,
7 dragOffsetFromLeft = DEFAULT_DRAG_OFFSET,
8 dragOffsetFromRight = -DEFAULT_DRAG_OFFSET,
9+ failOffsetY,
10 friction = DEFAULT_FRICTION,
11 overshootFriction = DEFAULT_OVERSHOOT_FRICTION,
12 onSwipeableOpenStartDrag,
13 onSwipeableCloseStartDrag,
14 onSwipeableWillOpen,
15+ onSwipeableRelease,
16 onSwipeableWillClose,
17 onSwipeableOpen,
18 onSwipeableClose,
19@@ -276,8 +278,12 @@ const Swipeable = props => {
20 toValue = -rightWidth.value;
21 }
22 }
23+ // Let a UI-thread full-swipe commit take over before the return spring.
24+ if (onSwipeableRelease?.(appliedTranslation)) {
25+ return;
26+ }
27 animateRow(toValue, velocityX / friction);
28- }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag]);
29+ }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag, onSwipeableRelease, appliedTranslation]);
30 const close = useCallback(() => {
31 'worklet';
32
33@@ -302,6 +308,7 @@ const Swipeable = props => {
34 enabled: enabled ?? true,
35 enableTrackpadTwoFingerGesture: enableTrackpadTwoFingerGesture,
36 activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft],
37+ failOffsetY,
38 simultaneousWith,
39 requireToFail,
40 block,
41diff --git a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts
42index e97b6dbbced1bcbd359cf35293b7f10cbf13b0e7..de07f26423bee66ecd7d0f9bb33233bcb3215751 100644
43--- a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts
44+++ b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts
45@@ -59,6 +59,12 @@ export type SwipeableProps = {
46 * Called when action panel is closed.
47 */
48 onSwipeableClose?: (direction: SwipeDirection.LEFT | SwipeDirection.RIGHT) => void;
49+ /**
50+ * UI-thread worklet called on release before the built-in spring starts.
51+ * Return true to consume the release and take ownership of translation.
52+ * The caller must reset or remove the row after its custom animation.
53+ */
54+ onSwipeableRelease?: (translation: SharedValue<number>) => boolean;
55 /**
56 * Called when action panel starts animating on open (either right or left).
57 */
58@@ -150,6 +156,13 @@ export type SwipeableProps = {
59 * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10.
60 */
61 dragOffsetFromRight?: number;
62+ /**
63+ * Range along the Y axis (in points) that, once exceeded before the swipe
64+ * activates, makes the pan fail. Lets vertically-dominant pans (list
65+ * scrolling, especially trackpad two-finger pans) skip the swipe entirely.
66+ * Passed straight to the underlying Pan gesture's `failOffsetY`.
67+ */
68+ failOffsetY?: number | [start: number, end: number];
69 }>;
70 export interface SwipeableMethods {
71 close: () => void;
72diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
73index 4881db1dc7575adc6610569035a8990cee414f3e..8431b738a9ffd09df48e1106b2bd5b62251d536c 100644
74--- a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
75+++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
76@@ -61,11 +61,13 @@ const Swipeable = (props: SwipeableProps) => {
77 enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE,
78 dragOffsetFromLeft = DEFAULT_DRAG_OFFSET,
79 dragOffsetFromRight = -DEFAULT_DRAG_OFFSET,
80+ failOffsetY,
81 friction = DEFAULT_FRICTION,
82 overshootFriction = DEFAULT_OVERSHOOT_FRICTION,
83 onSwipeableOpenStartDrag,
84 onSwipeableCloseStartDrag,
85 onSwipeableWillOpen,
86+ onSwipeableRelease,
87 onSwipeableWillClose,
88 onSwipeableOpen,
89 onSwipeableClose,
90@@ -491,6 +493,11 @@ const Swipeable = (props: SwipeableProps) => {
91 }
92 }
93
94+ // Let a UI-thread full-swipe commit take over before the return spring.
95+ if (onSwipeableRelease?.(appliedTranslation)) {
96+ return;
97+ }
98+
99 animateRow(toValue, velocityX / friction);
100 },
101 [
102@@ -502,6 +509,8 @@ const Swipeable = (props: SwipeableProps) => {
103 rightWidth,
104 rowState,
105 userDrag,
106+ onSwipeableRelease,
107+ appliedTranslation,
108 ]
109 );
110
111@@ -530,6 +539,7 @@ const Swipeable = (props: SwipeableProps) => {
112 enabled: enabled ?? true,
113 enableTrackpadTwoFingerGesture: enableTrackpadTwoFingerGesture,
114 activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft],
115+ failOffsetY,
116 simultaneousWith,
117 requireToFail,
118 block,
119diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts
120index 1e1246f709feb04c01cf3221472cc1219a8fffac..f526d4fe2d2738becbe8d29c59d1105afa11ae60 100644
121--- a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts
122+++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts
123@@ -77,6 +77,13 @@ export type SwipeableProps = {
124 direction: SwipeDirection.LEFT | SwipeDirection.RIGHT
125 ) => void;
126
127+ /**
128+ * UI-thread worklet called on release before the built-in spring starts.
129+ * Return true to consume the release and take ownership of translation.
130+ * The caller must reset or remove the row after its custom animation.
131+ */
132+ onSwipeableRelease?: (translation: SharedValue<number>) => boolean;
133+
134 /**
135 * Called when action panel starts animating on open (either right or left).
136 */
137@@ -196,6 +203,14 @@ export type SwipeableProps = {
138 * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10.
139 */
140 dragOffsetFromRight?: number;
141+
142+ /**
143+ * Range along the Y axis (in points) that, once exceeded before the swipe
144+ * activates, makes the pan fail. Lets vertically-dominant pans (list
145+ * scrolling, especially trackpad two-finger pans) skip the swipe entirely.
146+ * Passed straight to the underlying Pan gesture's `failOffsetY`.
147+ */
148+ failOffsetY?: number | [start: number, end: number];
149 }>;
150
151 export interface SwipeableMethods {
152diff --git a/apple/Handlers/RNPanHandler.m b/apple/Handlers/RNPanHandler.m
153--- a/apple/Handlers/RNPanHandler.m
154+++ b/apple/Handlers/RNPanHandler.m
155@@ -31,6 +31,7 @@
156 @property (nonatomic) CGFloat failOffsetYStart;
157 @property (nonatomic) CGFloat failOffsetYEnd;
158 @property (nonatomic) CGFloat activateAfterLongPress;
159+@property (nonatomic) BOOL handlingTouchStart;
160
161 #if !TARGET_OS_OSX && !TARGET_OS_TV
162 @property (atomic, readonly, strong) RNGHStylusData *stylusData;
163@@ -126,9 +127,14 @@
164
165 - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event
166 {
167- if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) {
168- self.state = UIGestureRecognizerStateFailed;
169- return;
170+ if (self.state == UIGestureRecognizerStatePossible) {
171+ self.handlingTouchStart = touches.count > 0;
172+ BOOL shouldBegin = [self.delegate gestureRecognizerShouldBegin:self];
173+ self.handlingTouchStart = NO;
174+ if (!shouldBegin) {
175+ self.state = UIGestureRecognizerStateFailed;
176+ return;
177+ }
178 }
179
180 if (touches.count == 0) {
181@@ -476,6 +482,23 @@
182
183 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
184 {
185+#if !TARGET_OS_OSX && !TARGET_OS_TV
186+ RNBetterPanGestureRecognizer *recognizer = (RNBetterPanGestureRecognizer *)gestureRecognizer;
187+ if (!recognizer.handlingTouchStart && recognizer.allowedScrollTypesMask != 0 && recognizer.numberOfTouches == 0) {
188+ // Trackpad scroll events can begin without touchesMoved enforcing failure
189+ // offsets. Reject vertical scrolling before a horizontal row pan cancels
190+ // the enclosing UIScrollView. The manual touch-start preflight above has
191+ // no current translation yet, so leave that path to the touch callbacks.
192+ CGPoint translation = [recognizer translationInView:recognizer.view.window];
193+ BOOL horizontalOnly = (!isnan(recognizer.activeOffsetXStart) || !isnan(recognizer.activeOffsetXEnd)) &&
194+ isnan(recognizer.activeOffsetYStart) && isnan(recognizer.activeOffsetYEnd);
195+ if ([recognizer shouldFailUnderCustomCriteria] ||
196+ (horizontalOnly && fabs(translation.y) > fabs(translation.x))) {
197+ return NO;
198+ }
199+ }
200+#endif
201+
202 RNGestureHandlerState savedState = _lastState;
203 BOOL shouldBegin = [super gestureRecognizerShouldBegin:gestureRecognizer];
204 _lastState = savedState;