react-native-screen-transitions 3.3.0-beta.3 → 3.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +95 -31
  2. package/lib/commonjs/shared/animation/snap-to.js +2 -0
  3. package/lib/commonjs/shared/animation/snap-to.js.map +1 -1
  4. package/lib/commonjs/shared/components/create-transition-aware-component.js +20 -18
  5. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  6. package/lib/commonjs/shared/components/screen-container.js +59 -6
  7. package/lib/commonjs/shared/components/screen-container.js.map +1 -1
  8. package/lib/commonjs/shared/constants.js +8 -1
  9. package/lib/commonjs/shared/constants.js.map +1 -1
  10. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +49 -39
  11. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  12. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js +110 -61
  13. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  14. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +67 -70
  15. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  16. package/lib/commonjs/shared/providers/gestures.provider.js +112 -5
  17. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  18. package/lib/commonjs/shared/types/ownership.types.js +71 -0
  19. package/lib/commonjs/shared/types/ownership.types.js.map +1 -0
  20. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +72 -128
  21. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  22. package/lib/commonjs/shared/utils/gesture/compute-claimed-directions.js +81 -0
  23. package/lib/commonjs/shared/utils/gesture/compute-claimed-directions.js.map +1 -0
  24. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js +1 -1
  25. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js.map +1 -1
  26. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js +48 -0
  27. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js.map +1 -0
  28. package/lib/commonjs/shared/utils/gesture/resolve-ownership.js +87 -0
  29. package/lib/commonjs/shared/utils/gesture/resolve-ownership.js.map +1 -0
  30. package/lib/commonjs/shared/utils/gesture/velocity.js +16 -5
  31. package/lib/commonjs/shared/utils/gesture/velocity.js.map +1 -1
  32. package/lib/module/shared/animation/snap-to.js +1 -0
  33. package/lib/module/shared/animation/snap-to.js.map +1 -1
  34. package/lib/module/shared/components/create-transition-aware-component.js +20 -18
  35. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  36. package/lib/module/shared/components/screen-container.js +59 -7
  37. package/lib/module/shared/components/screen-container.js.map +1 -1
  38. package/lib/module/shared/constants.js +7 -0
  39. package/lib/module/shared/constants.js.map +1 -1
  40. package/lib/module/shared/hooks/gestures/use-build-gestures.js +49 -39
  41. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  42. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js +112 -63
  43. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  44. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +68 -70
  45. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  46. package/lib/module/shared/providers/gestures.provider.js +112 -5
  47. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  48. package/lib/module/shared/types/ownership.types.js +67 -0
  49. package/lib/module/shared/types/ownership.types.js.map +1 -0
  50. package/lib/module/shared/utils/gesture/check-gesture-activation.js +70 -126
  51. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  52. package/lib/module/shared/utils/gesture/compute-claimed-directions.js +77 -0
  53. package/lib/module/shared/utils/gesture/compute-claimed-directions.js.map +1 -0
  54. package/lib/module/shared/utils/gesture/determine-snap-target.js +1 -1
  55. package/lib/module/shared/utils/gesture/determine-snap-target.js.map +1 -1
  56. package/lib/module/shared/utils/gesture/find-collapse-target.js +44 -0
  57. package/lib/module/shared/utils/gesture/find-collapse-target.js.map +1 -0
  58. package/lib/module/shared/utils/gesture/resolve-ownership.js +83 -0
  59. package/lib/module/shared/utils/gesture/resolve-ownership.js.map +1 -0
  60. package/lib/module/shared/utils/gesture/velocity.js +16 -5
  61. package/lib/module/shared/utils/gesture/velocity.js.map +1 -1
  62. package/lib/typescript/shared/animation/snap-to.d.ts.map +1 -1
  63. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  64. package/lib/typescript/shared/components/screen-container.d.ts.map +1 -1
  65. package/lib/typescript/shared/constants.d.ts +6 -0
  66. package/lib/typescript/shared/constants.d.ts.map +1 -1
  67. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +15 -3
  68. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  69. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts +52 -2
  70. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts.map +1 -1
  71. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +11 -6
  72. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  73. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts +1 -1
  74. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts.map +1 -1
  75. package/lib/typescript/shared/providers/gestures.provider.d.ts +27 -2
  76. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  77. package/lib/typescript/shared/types/ownership.types.d.ts +52 -0
  78. package/lib/typescript/shared/types/ownership.types.d.ts.map +1 -0
  79. package/lib/typescript/shared/types/screen.types.d.ts +22 -1
  80. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  81. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +23 -19
  82. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  83. package/lib/typescript/shared/utils/gesture/compute-claimed-directions.d.ts +23 -0
  84. package/lib/typescript/shared/utils/gesture/compute-claimed-directions.d.ts.map +1 -0
  85. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts +5 -1
  86. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts.map +1 -1
  87. package/lib/typescript/shared/utils/gesture/find-collapse-target.d.ts +17 -0
  88. package/lib/typescript/shared/utils/gesture/find-collapse-target.d.ts.map +1 -0
  89. package/lib/typescript/shared/utils/gesture/resolve-ownership.d.ts +36 -0
  90. package/lib/typescript/shared/utils/gesture/resolve-ownership.d.ts.map +1 -0
  91. package/lib/typescript/shared/utils/gesture/velocity.d.ts.map +1 -1
  92. package/package.json +121 -120
  93. package/src/shared/animation/snap-to.ts +1 -0
  94. package/src/shared/components/create-transition-aware-component.tsx +28 -25
  95. package/src/shared/components/screen-container.tsx +69 -7
  96. package/src/shared/constants.ts +7 -0
  97. package/src/shared/hooks/gestures/use-build-gestures.tsx +80 -44
  98. package/src/shared/hooks/gestures/use-screen-gesture-handlers.ts +147 -71
  99. package/src/shared/hooks/gestures/use-scroll-registry.tsx +94 -86
  100. package/src/shared/hooks/use-backdrop-pointer-events.ts +1 -1
  101. package/src/shared/providers/gestures.provider.tsx +166 -5
  102. package/src/shared/types/ownership.types.ts +77 -0
  103. package/src/shared/types/screen.types.ts +24 -1
  104. package/src/shared/utils/gesture/check-gesture-activation.ts +82 -116
  105. package/src/shared/utils/gesture/compute-claimed-directions.ts +93 -0
  106. package/src/shared/utils/gesture/determine-snap-target.ts +6 -2
  107. package/src/shared/utils/gesture/find-collapse-target.ts +42 -0
  108. package/src/shared/utils/gesture/resolve-ownership.ts +110 -0
  109. package/src/shared/utils/gesture/velocity.ts +16 -6
  110. package/src/shared/__tests__/bounds.store.test.ts +0 -394
  111. package/src/shared/__tests__/derivations.test.ts +0 -156
  112. package/src/shared/__tests__/determine-dismissal.test.ts +0 -111
  113. package/src/shared/__tests__/determine-snap-target.test.ts +0 -268
  114. package/src/shared/__tests__/geometry.test.ts +0 -130
  115. package/src/shared/__tests__/gesture-activation.test.ts +0 -471
  116. package/src/shared/__tests__/gesture.velocity.test.ts +0 -131
  117. package/src/shared/__tests__/history.store.test.ts +0 -550
  118. package/src/shared/__tests__/sync-routes-with-removed.test.ts +0 -137
  119. package/src/shared/__tests__/validate-snap-points.test.ts +0 -125
@@ -7,6 +7,7 @@ import {
7
7
  GestureOffsetState,
8
8
  type SideActivation,
9
9
  } from "../../types/gesture.types";
10
+ import type { Direction } from "../../types/ownership.types";
10
11
  import type { Layout } from "../../types/screen.types";
11
12
 
12
13
  type Directions = {
@@ -311,129 +312,94 @@ export const applyOffsetRules = ({
311
312
  };
312
313
  };
313
314
 
314
- interface ScrollAwareActivationParams {
315
- swipeInfo: {
316
- isSwipingDown: boolean;
317
- isSwipingUp: boolean;
318
- isSwipingRight: boolean;
319
- isSwipingLeft: boolean;
320
- };
321
- directions: Directions;
322
- scrollConfig: ScrollConfig | null;
323
- hasSnapPoints?: boolean;
324
- canExpandMore?: boolean;
325
- }
326
-
327
- type GestureDirection =
328
- | "vertical"
329
- | "vertical-inverted"
330
- | "horizontal"
331
- | "horizontal-inverted";
332
-
333
315
  /**
334
- * Checks if a gesture should activate based on scroll position.
335
- * Returns the direction to activate for, or null if activation should not occur.
316
+ * Checks if a ScrollView is at its boundary for the given swipe direction.
317
+ * This is a simplified boundary check that respects axis isolation.
318
+ *
319
+ * Per the spec:
320
+ * - A vertical ScrollView never yields to horizontal gestures
321
+ * - A horizontal ScrollView never yields to vertical gestures
322
+ * - ScrollView must be at boundary before yielding control
323
+ *
324
+ * For snap point sheets, the boundary depends on where the sheet originates from:
325
+ * - Bottom sheet (vertical): scrollY = 0 (top/base)
326
+ * - Top sheet (verticalInverted): scrollY >= maxY (bottom/end)
327
+ * - Right drawer (horizontal): scrollX = 0 (left/base)
328
+ * - Left drawer (horizontalInverted): scrollX >= maxX (right/end)
329
+ *
330
+ * The rule: "when the ScrollView can't scroll any further in the direction
331
+ * the sheet came from, yield to the gesture."
332
+ *
333
+ * @param scrollConfig - The current scroll state
334
+ * @param direction - The swipe direction to check
335
+ * @param snapAxisInverted - For snap point sheets, whether the axis is inverted (top sheet / left drawer)
336
+ * @returns true if at boundary (gesture should activate), false otherwise
336
337
  */
337
- export function checkScrollAwareActivation({
338
- swipeInfo,
339
- directions,
340
- scrollConfig,
341
- hasSnapPoints,
342
- canExpandMore,
343
- }: ScrollAwareActivationParams): {
344
- shouldActivate: boolean;
345
- direction: GestureDirection | null;
346
- } {
338
+ export function checkScrollBoundary(
339
+ scrollConfig: ScrollConfig | null,
340
+ direction: Direction,
341
+ snapAxisInverted?: boolean,
342
+ ): boolean {
347
343
  "worklet";
348
344
 
349
- const { isSwipingDown, isSwipingUp, isSwipingRight, isSwipingLeft } =
350
- swipeInfo;
351
-
352
- // Extract scroll values from config
353
- const scrollX = scrollConfig?.x ?? 0;
354
- const scrollY = scrollConfig?.y ?? 0;
355
- const maxScrollX = scrollConfig
356
- ? scrollConfig.contentWidth - scrollConfig.layoutWidth
357
- : 0;
358
- const maxScrollY = scrollConfig
359
- ? scrollConfig.contentHeight - scrollConfig.layoutHeight
360
- : 0;
361
- const snapAxisInverted = directions.snapAxisInverted;
362
-
363
- // With snap points, gestures should only activate based on the PRIMARY scroll edge
364
- // (the edge where the sheet originates from), not the opposite edge.
365
- // This prevents the auto-enabled opposite direction from hijacking scrolls.
366
- if (hasSnapPoints) {
367
- const isVerticalAxis = directions.vertical || directions.verticalInverted;
368
- const isHorizontalAxis =
369
- directions.horizontal || directions.horizontalInverted;
370
-
371
- if (isVerticalAxis) {
372
- if (snapAxisInverted) {
373
- // Sheet from TOP (vertical-inverted): only activate at scroll BOTTOM
374
- if (scrollY >= maxScrollY) {
375
- if (isSwipingUp) {
376
- return { shouldActivate: true, direction: "vertical-inverted" };
377
- }
378
- if (isSwipingDown && canExpandMore) {
379
- return { shouldActivate: true, direction: "vertical" };
380
- }
381
- }
382
- } else {
383
- // Sheet from BOTTOM (vertical): only activate at scroll TOP
384
- if (scrollY <= 0) {
385
- if (isSwipingDown) {
386
- return { shouldActivate: true, direction: "vertical" };
387
- }
388
- if (isSwipingUp && canExpandMore) {
389
- return { shouldActivate: true, direction: "vertical-inverted" };
390
- }
391
- }
392
- }
393
- }
394
-
395
- if (isHorizontalAxis) {
396
- if (snapAxisInverted) {
397
- // Sheet from LEFT (horizontal-inverted): only activate at scroll RIGHT
398
- if (scrollX >= maxScrollX) {
399
- if (isSwipingLeft) {
400
- return { shouldActivate: true, direction: "horizontal-inverted" };
401
- }
402
- if (isSwipingRight && canExpandMore) {
403
- return { shouldActivate: true, direction: "horizontal" };
404
- }
405
- }
406
- } else {
407
- // Sheet from RIGHT (horizontal): only activate at scroll LEFT
408
- if (scrollX <= 0) {
409
- if (isSwipingRight) {
410
- return { shouldActivate: true, direction: "horizontal" };
411
- }
412
- if (isSwipingLeft && canExpandMore) {
413
- return { shouldActivate: true, direction: "horizontal-inverted" };
414
- }
415
- }
416
- }
417
- }
418
-
419
- return { shouldActivate: false, direction: null };
345
+ if (!scrollConfig) {
346
+ // No scroll config means no ScrollView - allow gesture
347
+ return true;
420
348
  }
421
349
 
422
- if (directions.vertical && isSwipingDown && scrollY <= 0) {
423
- return { shouldActivate: true, direction: "vertical" };
424
- }
425
-
426
- if (directions.horizontal && isSwipingRight && scrollX <= 0) {
427
- return { shouldActivate: true, direction: "horizontal" };
428
- }
429
-
430
- if (directions.verticalInverted && isSwipingUp && scrollY >= maxScrollY) {
431
- return { shouldActivate: true, direction: "vertical-inverted" };
350
+ const {
351
+ x: scrollX,
352
+ y: scrollY,
353
+ contentWidth,
354
+ contentHeight,
355
+ layoutWidth,
356
+ layoutHeight,
357
+ } = scrollConfig;
358
+
359
+ // Calculate max scroll values
360
+ const maxScrollX = Math.max(0, contentWidth - layoutWidth);
361
+ const maxScrollY = Math.max(0, contentHeight - layoutHeight);
362
+
363
+ // For snap point sheets (snapAxisInverted is defined), boundary depends on sheet origin
364
+ // Even if content isn't scrollable, respect bounce/overscroll state
365
+ if (snapAxisInverted !== undefined) {
366
+ const isVerticalDirection =
367
+ direction === "vertical" || direction === "vertical-inverted";
368
+
369
+ if (isVerticalDirection) {
370
+ // Bottom sheet (not inverted): boundary at scroll top
371
+ // Top sheet (inverted): boundary at scroll bottom
372
+ return snapAxisInverted ? scrollY >= maxScrollY : scrollY <= 0;
373
+ }
374
+ // Horizontal direction
375
+ // Right drawer (not inverted): boundary at scroll left
376
+ // Left drawer (inverted): boundary at scroll right
377
+ return snapAxisInverted ? scrollX >= maxScrollX : scrollX <= 0;
432
378
  }
433
379
 
434
- if (directions.horizontalInverted && isSwipingLeft && scrollX >= maxScrollX) {
435
- return { shouldActivate: true, direction: "horizontal-inverted" };
380
+ // Non-sheet screens: each direction has its own boundary
381
+ switch (direction) {
382
+ case "vertical":
383
+ // Swipe down - check if at top of vertical scroll
384
+ // Even if content isn't scrollable, respect bounce/overscroll state
385
+ return scrollY <= 0;
386
+
387
+ case "vertical-inverted":
388
+ // Swipe up - check if at bottom of vertical scroll
389
+ // Even if content isn't scrollable, respect bounce/overscroll state
390
+ return scrollY >= maxScrollY;
391
+
392
+ case "horizontal":
393
+ // Swipe right - check if at left of horizontal scroll
394
+ // Even if content isn't scrollable, respect bounce/overscroll state
395
+ return scrollX <= 0;
396
+
397
+ case "horizontal-inverted":
398
+ // Swipe left - check if at right of horizontal scroll
399
+ // Even if content isn't scrollable, respect bounce/overscroll state
400
+ return scrollX >= maxScrollX;
401
+
402
+ default:
403
+ return true;
436
404
  }
437
-
438
- return { shouldActivate: false, direction: null };
439
405
  }
@@ -0,0 +1,93 @@
1
+ import type { GestureDirection } from "../../types/gesture.types";
2
+ import {
3
+ type ClaimedDirections,
4
+ type Direction,
5
+ NO_CLAIMS,
6
+ } from "../../types/ownership.types";
7
+
8
+ /**
9
+ * Computes which directions a screen claims ownership of.
10
+ *
11
+ * A screen claims a direction when:
12
+ * 1. gestureEnabled is true AND
13
+ * 2. gestureDirection includes that direction
14
+ *
15
+ * For snap points, both directions on the axis are claimed automatically.
16
+ * This is because a snap point sheet handles both expand (inverse) and collapse (primary) gestures.
17
+ *
18
+ * @param gestureEnabled - Whether gestures are enabled for this screen
19
+ * @param gestureDirection - The gesture direction(s) configured for this screen
20
+ * @param hasSnapPoints - Whether this screen has snap points configured
21
+ * @returns The claimed directions for this screen
22
+ */
23
+ export function computeClaimedDirections(
24
+ gestureEnabled: boolean,
25
+ gestureDirection: GestureDirection | GestureDirection[] | undefined,
26
+ hasSnapPoints: boolean,
27
+ ): ClaimedDirections {
28
+ // If gestures are not enabled, claim nothing
29
+ if (!gestureEnabled) {
30
+ return NO_CLAIMS;
31
+ }
32
+
33
+ // Default to vertical if no direction specified
34
+ const direction = gestureDirection ?? "vertical";
35
+
36
+ // Normalize to array
37
+ const directions: GestureDirection[] = Array.isArray(direction)
38
+ ? direction
39
+ : [direction];
40
+
41
+ // Start with no claims
42
+ const claims: ClaimedDirections = {
43
+ vertical: false,
44
+ "vertical-inverted": false,
45
+ horizontal: false,
46
+ "horizontal-inverted": false,
47
+ };
48
+
49
+ // Process each direction
50
+ for (const dir of directions) {
51
+ if (dir === "bidirectional") {
52
+ // Bidirectional claims all four directions
53
+ claims.vertical = true;
54
+ claims["vertical-inverted"] = true;
55
+ claims.horizontal = true;
56
+ claims["horizontal-inverted"] = true;
57
+ } else {
58
+ // Claim the specific direction
59
+ claims[dir as Direction] = true;
60
+ }
61
+ }
62
+
63
+ // For snap points, claim both directions on the axis
64
+ // This enables both expand (inverse) and collapse/dismiss (primary) gestures
65
+ if (hasSnapPoints) {
66
+ const hasVerticalAxis = claims.vertical || claims["vertical-inverted"];
67
+ const hasHorizontalAxis =
68
+ claims.horizontal || claims["horizontal-inverted"];
69
+
70
+ if (hasVerticalAxis) {
71
+ claims.vertical = true;
72
+ claims["vertical-inverted"] = true;
73
+ }
74
+ if (hasHorizontalAxis) {
75
+ claims.horizontal = true;
76
+ claims["horizontal-inverted"] = true;
77
+ }
78
+ }
79
+
80
+ return claims;
81
+ }
82
+
83
+ /**
84
+ * Checks if any direction is claimed.
85
+ */
86
+ export function claimsAnyDirection(claims: ClaimedDirections): boolean {
87
+ return (
88
+ claims.vertical ||
89
+ claims["vertical-inverted"] ||
90
+ claims.horizontal ||
91
+ claims["horizontal-inverted"]
92
+ );
93
+ }
@@ -5,7 +5,11 @@ interface DetermineSnapTargetProps {
5
5
  velocity: number;
6
6
  /** Screen dimension along the snap axis (width or height) */
7
7
  dimension: number;
8
- /** How much velocity affects the snap decision (0-1). Default 0.15 */
8
+ /**
9
+ * How much velocity affects the snap decision.
10
+ * Lower values = more deliberate/iOS-like, higher values = more responsive to flicks.
11
+ * @default 0.1
12
+ */
9
13
  velocityFactor?: number;
10
14
  /** Whether dismiss (progress=0) is allowed. Default true */
11
15
  canDismiss?: boolean;
@@ -28,7 +32,7 @@ export function determineSnapTarget({
28
32
  snapPoints,
29
33
  velocity,
30
34
  dimension,
31
- velocityFactor = 0.15,
35
+ velocityFactor = 0.1,
32
36
  canDismiss = true,
33
37
  }: DetermineSnapTargetProps): DetermineSnapTargetResult {
34
38
  "worklet";
@@ -0,0 +1,42 @@
1
+ import { EPSILON } from "../../constants";
2
+
3
+ interface FindCollapseTargetResult {
4
+ target: number;
5
+ shouldDismiss: boolean;
6
+ }
7
+
8
+ /**
9
+ * Finds the next lower snap point for backdrop collapse behavior.
10
+ *
11
+ * - If above min snap: returns next lower snap point
12
+ * - If at or below min snap: returns 0 (dismiss) if canDismiss, else stays at min
13
+ *
14
+ * @param currentProgress - Current animation progress
15
+ * @param snapPoints - Array of snap points
16
+ * @param canDismiss - Whether dismissing is allowed
17
+ */
18
+ export function findCollapseTarget(
19
+ currentProgress: number,
20
+ snapPoints: number[],
21
+ canDismiss: boolean,
22
+ ): FindCollapseTargetResult {
23
+ "worklet";
24
+
25
+ const sorted = [...snapPoints].sort((a, b) => a - b);
26
+ const minSnap = sorted[0];
27
+
28
+ // Find next lower snap point
29
+ for (let i = sorted.length - 1; i >= 0; i--) {
30
+ if (sorted[i] < currentProgress - EPSILON) {
31
+ return { target: sorted[i], shouldDismiss: false };
32
+ }
33
+ }
34
+
35
+ // At or below min snap → dismiss if allowed
36
+ if (canDismiss) {
37
+ return { target: 0, shouldDismiss: true };
38
+ }
39
+
40
+ // Can't dismiss, stay at min
41
+ return { target: minSnap, shouldDismiss: false };
42
+ }
@@ -0,0 +1,110 @@
1
+ import type { ClaimedDirections, Direction } from "../../types/ownership.types";
2
+ import {
3
+ DIRECTIONS,
4
+ type DirectionOwnership,
5
+ NO_OWNERSHIP,
6
+ type OwnershipStatus,
7
+ } from "../../types/ownership.types";
8
+
9
+ /**
10
+ * Minimal interface for ancestor context needed for ownership resolution.
11
+ * This allows the function to be used without importing the full GestureContextType.
12
+ */
13
+ export interface AncestorClaimsContext {
14
+ claimedDirections: ClaimedDirections;
15
+ ancestorContext: AncestorClaimsContext | null;
16
+ }
17
+
18
+ /**
19
+ * Resolves ownership status for all directions relative to the current screen.
20
+ *
21
+ * For each direction:
22
+ * 1. If the current screen claims it → 'self' (should activate)
23
+ * 2. Else, walk up ancestors looking for a claim → 'ancestor' (should fail to bubble)
24
+ * 3. If no one claims it → 'none' (should fail, no gesture response)
25
+ *
26
+ * This is computed during render (JS thread) and the result can be safely
27
+ * used in worklets since it's a plain object.
28
+ *
29
+ * @param selfClaims - The directions claimed by the current screen
30
+ * @param ancestorContext - The ancestor context chain (can be null if no ancestors)
31
+ * @returns Ownership status for all four directions
32
+ */
33
+ export function resolveOwnership(
34
+ selfClaims: ClaimedDirections,
35
+ ancestorContext: AncestorClaimsContext | null,
36
+ ): DirectionOwnership {
37
+ const result: DirectionOwnership = { ...NO_OWNERSHIP };
38
+
39
+ for (const direction of DIRECTIONS) {
40
+ result[direction] = resolveDirectionOwnership(
41
+ direction,
42
+ selfClaims,
43
+ ancestorContext,
44
+ );
45
+ }
46
+
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Resolves ownership for a single direction.
52
+ */
53
+ function resolveDirectionOwnership(
54
+ direction: Direction,
55
+ selfClaims: ClaimedDirections,
56
+ ancestorContext: AncestorClaimsContext | null,
57
+ ): OwnershipStatus {
58
+ // Check self first
59
+ if (selfClaims[direction]) {
60
+ return "self";
61
+ }
62
+
63
+ // Walk ancestors looking for a claim
64
+ let ancestor = ancestorContext;
65
+ while (ancestor) {
66
+ if (ancestor.claimedDirections?.[direction]) {
67
+ return "ancestor";
68
+ }
69
+ ancestor = ancestor.ancestorContext;
70
+ }
71
+
72
+ // No one claims this direction
73
+ return "none";
74
+ }
75
+
76
+ /**
77
+ * Finds the nearest ancestor (or self if isCurrentOwner) that claims any direction.
78
+ * Used for setting up native gesture relationships.
79
+ *
80
+ * @param selfClaimsAny - Whether the current screen claims any direction
81
+ * @param ancestorContext - The ancestor context chain
82
+ * @returns The nearest context that claims a direction, or null
83
+ */
84
+ export function findNearestOwner(
85
+ selfClaimsAny: boolean,
86
+ ancestorContext: AncestorClaimsContext | null,
87
+ ): AncestorClaimsContext | null {
88
+ // If self claims any direction, self is the nearest owner
89
+ // (but we return null since this is used for finding ANCESTOR owners)
90
+ if (selfClaimsAny) {
91
+ return null;
92
+ }
93
+
94
+ // Walk ancestors looking for one that claims any direction
95
+ let ancestor = ancestorContext;
96
+ while (ancestor) {
97
+ const claims = ancestor.claimedDirections;
98
+ if (
99
+ claims?.vertical ||
100
+ claims?.["vertical-inverted"] ||
101
+ claims?.horizontal ||
102
+ claims?.["horizontal-inverted"]
103
+ ) {
104
+ return ancestor;
105
+ }
106
+ ancestor = ancestor.ancestorContext;
107
+ }
108
+
109
+ return null;
110
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  PanGestureHandlerEventPayload,
4
4
  } from "react-native-gesture-handler";
5
5
  import { clamp } from "react-native-reanimated";
6
+ import { ANIMATION_SNAP_THRESHOLD, EPSILON } from "../../constants";
6
7
  import type { AnimationStoreMap } from "../../stores/animation.store";
7
8
 
8
9
  interface CalculateProgressProps {
@@ -19,7 +20,6 @@ interface CalculateProgressProps {
19
20
  }
20
21
 
21
22
  const MAX_VELOCITY_MAGNITUDE = 3.2;
22
- const NEAR_ZERO_THRESHOLD = 0.01;
23
23
 
24
24
  /**
25
25
  * Converts velocity from pixels/second to normalized units/second (0-1 range)
@@ -53,7 +53,7 @@ const calculateRestoreVelocity = (
53
53
  ) => {
54
54
  "worklet";
55
55
 
56
- if (Math.abs(currentValueNormalized) < NEAR_ZERO_THRESHOLD) return 0;
56
+ if (Math.abs(currentValueNormalized) < ANIMATION_SNAP_THRESHOLD) return 0;
57
57
 
58
58
  const directionTowardZero = Math.sign(currentValueNormalized) || 1;
59
59
  const clampedVelocity = Math.min(Math.abs(baseVelocityNormalized), 1);
@@ -132,16 +132,26 @@ const shouldPassDismissalThreshold = (
132
132
  "worklet";
133
133
 
134
134
  const normalizedTranslation = translationPixels / Math.max(1, screenSize);
135
+
136
+ // If translation is essentially zero, velocity alone shouldn't trigger dismissal.
137
+ // User must have meaningfully moved in the dismiss direction.
138
+ if (Math.abs(normalizedTranslation) < EPSILON) {
139
+ return false;
140
+ }
141
+
135
142
  const normalizedVelocity = normalize(velocityPixelsPerSecond, screenSize);
136
143
 
137
144
  const projectedNormalizedPosition =
138
145
  normalizedTranslation + normalizedVelocity * velocityWeight;
139
146
 
140
- const exceedsThreshold = Math.abs(projectedNormalizedPosition) > 0.5;
141
-
142
- const hasMovement = translationPixels !== 0 || velocityPixelsPerSecond !== 0;
147
+ // The dismiss direction is determined by the sign of the translation.
148
+ // Multiplying by this sign normalizes the projection so "toward dismiss" is always positive.
149
+ // This prevents dismissal when the user drags back (opposing velocity flips projection negative).
150
+ const dismissSign = Math.sign(translationPixels);
151
+ const projectedInDismissDirection = projectedNormalizedPosition * dismissSign;
152
+ const exceedsThreshold = projectedInDismissDirection > 0.5;
143
153
 
144
- return exceedsThreshold && hasMovement;
154
+ return exceedsThreshold;
145
155
  };
146
156
 
147
157
  export const velocity = {