react-native-gesture-image-viewer 2.4.0 → 3.0.0-beta.0

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 (133) hide show
  1. package/README.md +5 -3
  2. package/lib/module/GestureViewer.js +88 -146
  3. package/lib/module/GestureViewer.js.map +1 -1
  4. package/lib/module/GestureViewerManager.js +49 -105
  5. package/lib/module/GestureViewerManager.js.map +1 -1
  6. package/lib/module/gestureViewerAnimation.js +18 -0
  7. package/lib/module/gestureViewerAnimation.js.map +1 -0
  8. package/lib/module/gestureViewerPaging.js +66 -0
  9. package/lib/module/gestureViewerPaging.js.map +1 -0
  10. package/lib/module/gestureViewerRenderWindow.js +29 -0
  11. package/lib/module/gestureViewerRenderWindow.js.map +1 -0
  12. package/lib/module/getWebContentProps.js +6 -0
  13. package/lib/module/getWebContentProps.js.map +1 -0
  14. package/lib/module/getWebContentProps.web.js +8 -0
  15. package/lib/module/getWebContentProps.web.js.map +1 -0
  16. package/lib/module/platformTapGestures.js +4 -0
  17. package/lib/module/platformTapGestures.js.map +1 -0
  18. package/lib/module/platformTapGestures.web.js +4 -0
  19. package/lib/module/platformTapGestures.web.js.map +1 -0
  20. package/lib/module/renderWindow.js +160 -0
  21. package/lib/module/renderWindow.js.map +1 -0
  22. package/lib/module/useGestureViewer.js +386 -206
  23. package/lib/module/useGestureViewer.js.map +1 -1
  24. package/lib/module/useGestureViewerController.js +3 -3
  25. package/lib/module/useGestureViewerController.js.map +1 -1
  26. package/lib/module/useGestureViewerManagerBridge.js +85 -0
  27. package/lib/module/useGestureViewerManagerBridge.js.map +1 -0
  28. package/lib/module/useGestureViewerPaging.js +147 -79
  29. package/lib/module/useGestureViewerPaging.js.map +1 -1
  30. package/lib/module/useWebClickHandler.js +7 -0
  31. package/lib/module/useWebClickHandler.js.map +1 -0
  32. package/lib/module/useWebClickHandler.web.js +85 -0
  33. package/lib/module/useWebClickHandler.web.js.map +1 -0
  34. package/lib/module/useWebSingleTapTimer.js +12 -0
  35. package/lib/module/useWebSingleTapTimer.js.map +1 -0
  36. package/lib/module/useWebSingleTapTimer.web.js +25 -0
  37. package/lib/module/useWebSingleTapTimer.web.js.map +1 -0
  38. package/lib/module/utils/index.js +0 -77
  39. package/lib/module/utils/index.js.map +1 -1
  40. package/lib/module/utils/tapZoom.js +4 -3
  41. package/lib/module/utils/tapZoom.js.map +1 -1
  42. package/lib/typescript/src/GestureViewer.d.ts +1 -1
  43. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  44. package/lib/typescript/src/GestureViewerManager.d.ts +20 -20
  45. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  46. package/lib/typescript/src/gestureViewerAnimation.d.ts +15 -0
  47. package/lib/typescript/src/gestureViewerAnimation.d.ts.map +1 -0
  48. package/lib/typescript/src/gestureViewerPaging.d.ts +13 -0
  49. package/lib/typescript/src/gestureViewerPaging.d.ts.map +1 -0
  50. package/lib/typescript/src/gestureViewerRenderWindow.d.ts +15 -0
  51. package/lib/typescript/src/gestureViewerRenderWindow.d.ts.map +1 -0
  52. package/lib/typescript/src/getWebContentProps.d.ts +3 -0
  53. package/lib/typescript/src/getWebContentProps.d.ts.map +1 -0
  54. package/lib/typescript/src/getWebContentProps.web.d.ts +5 -0
  55. package/lib/typescript/src/getWebContentProps.web.d.ts.map +1 -0
  56. package/lib/typescript/src/platformTapGestures.d.ts +2 -0
  57. package/lib/typescript/src/platformTapGestures.d.ts.map +1 -0
  58. package/lib/typescript/src/platformTapGestures.web.d.ts +2 -0
  59. package/lib/typescript/src/platformTapGestures.web.d.ts.map +1 -0
  60. package/lib/typescript/src/renderWindow.d.ts +44 -0
  61. package/lib/typescript/src/renderWindow.d.ts.map +1 -0
  62. package/lib/typescript/src/types.d.ts +36 -53
  63. package/lib/typescript/src/types.d.ts.map +1 -1
  64. package/lib/typescript/src/useGestureViewer.d.ts +9 -9
  65. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  66. package/lib/typescript/src/useGestureViewerController.d.ts +1 -1
  67. package/lib/typescript/src/useGestureViewerManagerBridge.d.ts +28 -0
  68. package/lib/typescript/src/useGestureViewerManagerBridge.d.ts.map +1 -0
  69. package/lib/typescript/src/useGestureViewerPaging.d.ts +31 -2
  70. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -1
  71. package/lib/typescript/src/useWebClickHandler.d.ts +35 -0
  72. package/lib/typescript/src/useWebClickHandler.d.ts.map +1 -0
  73. package/lib/typescript/src/useWebClickHandler.web.d.ts +36 -0
  74. package/lib/typescript/src/useWebClickHandler.web.d.ts.map +1 -0
  75. package/lib/typescript/src/useWebSingleTapTimer.d.ts +6 -0
  76. package/lib/typescript/src/useWebSingleTapTimer.d.ts.map +1 -0
  77. package/lib/typescript/src/useWebSingleTapTimer.web.d.ts +6 -0
  78. package/lib/typescript/src/useWebSingleTapTimer.web.d.ts.map +1 -0
  79. package/lib/typescript/src/utils/index.d.ts +0 -20
  80. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  81. package/lib/typescript/src/utils/tapZoom.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/GestureViewer.tsx +104 -206
  84. package/src/GestureViewerManager.ts +77 -122
  85. package/src/gestureViewerAnimation.ts +18 -0
  86. package/src/gestureViewerPaging.ts +116 -0
  87. package/src/gestureViewerRenderWindow.ts +56 -0
  88. package/src/getWebContentProps.ts +5 -0
  89. package/src/getWebContentProps.web.ts +5 -0
  90. package/src/platformTapGestures.ts +1 -0
  91. package/src/platformTapGestures.web.ts +1 -0
  92. package/src/renderWindow.ts +253 -0
  93. package/src/types.ts +34 -100
  94. package/src/useGestureViewer.ts +557 -271
  95. package/src/useGestureViewerController.ts +3 -3
  96. package/src/useGestureViewerManagerBridge.ts +118 -0
  97. package/src/useGestureViewerPaging.ts +277 -114
  98. package/src/useWebClickHandler.ts +44 -0
  99. package/src/useWebClickHandler.web.ts +146 -0
  100. package/src/useWebSingleTapTimer.ts +10 -0
  101. package/src/useWebSingleTapTimer.web.ts +30 -0
  102. package/src/utils/index.ts +0 -102
  103. package/src/utils/tapZoom.ts +4 -3
  104. package/lib/module/WebPagingFixStyle.js +0 -25
  105. package/lib/module/WebPagingFixStyle.js.map +0 -1
  106. package/lib/module/scheduleInitialScroll.js +0 -21
  107. package/lib/module/scheduleInitialScroll.js.map +0 -1
  108. package/lib/module/useGestureViewerPaging.types.js +0 -4
  109. package/lib/module/useGestureViewerPaging.types.js.map +0 -1
  110. package/lib/module/useGestureViewerPaging.web.js +0 -291
  111. package/lib/module/useGestureViewerPaging.web.js.map +0 -1
  112. package/lib/module/utils/FlashList.js +0 -9
  113. package/lib/module/utils/FlashList.js.map +0 -1
  114. package/lib/module/utils/webScroll.js +0 -70
  115. package/lib/module/utils/webScroll.js.map +0 -1
  116. package/lib/typescript/src/WebPagingFixStyle.d.ts +0 -6
  117. package/lib/typescript/src/WebPagingFixStyle.d.ts.map +0 -1
  118. package/lib/typescript/src/scheduleInitialScroll.d.ts +0 -3
  119. package/lib/typescript/src/scheduleInitialScroll.d.ts.map +0 -1
  120. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +0 -42
  121. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +0 -1
  122. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +0 -3
  123. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +0 -1
  124. package/lib/typescript/src/utils/FlashList.d.ts +0 -2
  125. package/lib/typescript/src/utils/FlashList.d.ts.map +0 -1
  126. package/lib/typescript/src/utils/webScroll.d.ts +0 -18
  127. package/lib/typescript/src/utils/webScroll.d.ts.map +0 -1
  128. package/src/WebPagingFixStyle.tsx +0 -25
  129. package/src/scheduleInitialScroll.ts +0 -36
  130. package/src/useGestureViewerPaging.types.ts +0 -42
  131. package/src/useGestureViewerPaging.web.ts +0 -453
  132. package/src/utils/FlashList.ts +0 -7
  133. package/src/utils/webScroll.ts +0 -96
@@ -1,8 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { Platform, type View, useWindowDimensions } from 'react-native';
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { type View, useWindowDimensions } from 'react-native';
3
3
  import { Gesture, type GestureType } from 'react-native-gesture-handler';
4
4
  import {
5
5
  Easing,
6
+ cancelAnimation,
6
7
  interpolate,
7
8
  useAnimatedReaction,
8
9
  useAnimatedStyle,
@@ -12,28 +13,46 @@ import {
12
13
  } from 'react-native-reanimated';
13
14
  import { scheduleOnRN } from 'react-native-worklets';
14
15
 
16
+ import {
17
+ PAGE_SPRING_CONFIG,
18
+ SWIPE_THRESHOLD_RATIO,
19
+ SWIPE_VELOCITY_THRESHOLD,
20
+ } from './gestureViewerAnimation';
15
21
  import type GestureViewerManager from './GestureViewerManager';
22
+ import { normalizeHorizontalSwipeThreshold } from './gestureViewerPaging';
16
23
  import { registry } from './GestureViewerRegistry';
17
- import { scheduleInitialScroll } from './scheduleInitialScroll';
24
+ import { resolveGestureViewerRenderWindow } from './gestureViewerRenderWindow';
25
+ import { enableNativeTapGestures } from './platformTapGestures';
26
+ import {
27
+ type NavigationOptions,
28
+ getLogicalIndex,
29
+ getPageStride,
30
+ getVirtualIndexForLogicalIndex,
31
+ normalizePageSpacing,
32
+ clampIndex,
33
+ resolveNavigation,
34
+ shouldRunNavigationDuringTransition,
35
+ } from './renderWindow';
18
36
  import type { GestureViewerProps, TriggerRect } from './types';
37
+ import { useGestureViewerManagerBridge } from './useGestureViewerManagerBridge';
19
38
  import { useGestureViewerPaging } from './useGestureViewerPaging';
20
- import { createBoundsConstraint, createScrollAction } from './utils';
39
+ import { type EmitSingleTap, useWebClickHandler } from './useWebClickHandler';
40
+ import { useWebSingleTapTimer } from './useWebSingleTapTimer';
41
+ import { createBoundsConstraint } from './utils';
21
42
  import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
22
43
  import { applyTapZoomAtPoint } from './utils/tapZoom';
23
44
  import { calculateFocalPointTranslation, shouldAcceptFocalPoint } from './utils/zoom';
24
45
 
25
- type UseGestureViewerProps<ItemT, LC> = Omit<
26
- GestureViewerProps<ItemT, LC>,
27
- | 'renderItem'
28
- | 'renderContainer'
29
- | 'ListComponent'
30
- | 'listProps'
31
- | 'containerStyle'
32
- | 'backdropStyle'
33
- | 'enableSnapMode'
46
+ const isValidTriggerRect = (rect: TriggerRect | null): rect is TriggerRect => {
47
+ return !!rect && rect.width > 0 && rect.height > 0;
48
+ };
49
+
50
+ type UseGestureViewerProps<ItemT> = Omit<
51
+ GestureViewerProps<ItemT>,
52
+ 'renderItem' | 'renderContainer' | 'containerStyle' | 'backdropStyle'
34
53
  >;
35
54
 
36
- export const useGestureViewer = <ItemT, LC>({
55
+ export const useGestureViewer = <ItemT>({
37
56
  data,
38
57
  initialIndex = 0,
39
58
  onDismiss,
@@ -42,21 +61,35 @@ export const useGestureViewer = <ItemT, LC>({
42
61
  dismiss,
43
62
  enableDoubleTapZoom = true,
44
63
  enablePinchZoom = true,
45
- enableHorizontalSwipe = true,
64
+ horizontalSwipe,
46
65
  enablePanWhenZoomed = true,
47
66
  enableLoop = false,
48
67
  maxZoomScale = 2,
49
- itemSpacing = 0,
68
+ pageSpacing = 0,
69
+ windowSize = 3,
50
70
  height: customHeight,
51
71
  id = 'default',
52
72
  onDismissStart,
53
73
  triggerAnimation,
54
74
  autoPlay = false,
55
75
  autoPlayInterval = 3000,
56
- }: UseGestureViewerProps<ItemT, LC>) => {
76
+ }: UseGestureViewerProps<ItemT>) => {
57
77
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
58
78
  const width = customWidth || screenWidth;
59
79
  const height = customHeight || screenHeight;
80
+ const dataLength = data?.length || 0;
81
+ const normalizedPageSpacing = normalizePageSpacing(pageSpacing);
82
+ const pageStride = getPageStride(width, normalizedPageSpacing);
83
+ const initialCurrentIndex = clampIndex(initialIndex, dataLength);
84
+ const horizontalSwipeEnabled = horizontalSwipe?.enabled ?? true;
85
+ const horizontalSwipeDistanceThresholdRatio = normalizeHorizontalSwipeThreshold(
86
+ horizontalSwipe?.distanceThresholdRatio,
87
+ SWIPE_THRESHOLD_RATIO,
88
+ );
89
+ const horizontalSwipeVelocityThreshold = normalizeHorizontalSwipeThreshold(
90
+ horizontalSwipe?.velocityThreshold,
91
+ SWIPE_VELOCITY_THRESHOLD,
92
+ );
60
93
 
61
94
  const dismissGestureRef = useRef<GestureType>(undefined);
62
95
 
@@ -64,21 +97,30 @@ export const useGestureViewer = <ItemT, LC>({
64
97
  const [isRotated, setIsRotated] = useState(false);
65
98
  const [isPinching, setIsPinching] = useState(false);
66
99
  const [shouldStartTriggerAnimation, setShouldStartTriggerAnimation] = useState(false);
67
- const [manager, setManager] = useState<GestureViewerManager | null>(null);
68
- const [currentIndex, setCurrentIndex] = useState(initialIndex);
69
- const [activeTriggerNode, setActiveTriggerNode] = useState<View | null>(null);
100
+ const [currentIndex, setCurrentIndex] = useState(initialCurrentIndex);
101
+ const [centerVirtualIndex, setCenterVirtualIndex] = useState(initialCurrentIndex);
102
+ const [activeTriggerNode, setActiveTriggerNode] = useState<View | null>(() =>
103
+ registry.getActiveTriggerNode(id),
104
+ );
105
+ const [isTriggerOpening, setIsTriggerOpening] = useState(
106
+ () => !!registry.getActiveTriggerNode(id),
107
+ );
70
108
 
71
- const listRef = useRef<any>(null);
72
109
  const triggerRectRef = useRef<TriggerRect | null>(null);
73
- const pendingIndexRef = useRef(initialIndex);
110
+ const pendingIndexRef = useRef(initialCurrentIndex);
111
+ const currentIndexRef = useRef(initialCurrentIndex);
112
+ const centerVirtualIndexRef = useRef(initialCurrentIndex);
113
+ const previousInitialIndexRef = useRef(initialIndex);
114
+ const hasResolvedInitialIndexRef = useRef(dataLength > 0);
74
115
  const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
75
116
  const onSingleTapRef = useRef(onSingleTap);
76
117
  const dataRef = useRef(data);
77
- const managerRef = useRef(manager);
78
-
79
- const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
80
- return !!rect && rect.width > 0 && rect.height > 0;
81
- }, []);
118
+ const dataLengthRef = useRef(dataLength);
119
+ const enableLoopRef = useRef(enableLoop);
120
+ const managerRef = useRef<GestureViewerManager | null>(null);
121
+ const isZoomedRef = useRef(isZoomed);
122
+ const isRotatedRef = useRef(isRotated);
123
+ const isPinchingRef = useRef(isPinching);
82
124
 
83
125
  const initialTranslateY = useSharedValue(0);
84
126
  const initialTranslateX = useSharedValue(0);
@@ -87,22 +129,22 @@ export const useGestureViewer = <ItemT, LC>({
87
129
  const translateY = useSharedValue(0);
88
130
  const translateX = useSharedValue(0);
89
131
  const scale = useSharedValue(1);
90
- const backdropOpacity = useSharedValue(1);
91
132
  const rotation = useSharedValue(0);
92
133
 
93
134
  const triggerScale = useSharedValue(1);
94
135
  const triggerTranslateX = useSharedValue(0);
95
136
  const triggerTranslateY = useSharedValue(0);
96
- const triggerOpacity = useSharedValue(1);
137
+ const triggerOpacity = useSharedValue(activeTriggerNode ? 0 : 1);
138
+
139
+ const hasZoomChangeListeners = useSharedValue(false);
140
+ const hasRotationChangeListeners = useSharedValue(false);
97
141
 
98
142
  const lastFocalX = useSharedValue(0);
99
143
  const lastFocalY = useSharedValue(0);
100
144
  const startFocalX = useSharedValue(0);
101
145
  const startFocalY = useSharedValue(0);
102
146
  const hasActiveFocal = useSharedValue(false);
103
-
104
- const dataLength = data?.length || 0;
105
- const previousDataLengthRef = useRef(dataLength);
147
+ const { clearPendingWebSingleTap, scheduleWebSingleTap } = useWebSingleTapTimer();
106
148
 
107
149
  const animationConfig = useMemo(
108
150
  () => ({
@@ -130,28 +172,24 @@ export const useGestureViewer = <ItemT, LC>({
130
172
  ],
131
173
  );
132
174
 
133
- const adjustedInitialIndex = useMemo(() => {
134
- if (enableLoop && dataLength > 1) {
135
- return initialIndex + 1;
136
- }
137
-
138
- return initialIndex;
139
- }, [enableLoop, dataLength, initialIndex]);
175
+ const {
176
+ centerVirtualIndex: renderCenterVirtualIndex,
177
+ currentIndex: renderCurrentIndex,
178
+ slots: renderWindowSlots,
179
+ } = resolveGestureViewerRenderWindow({
180
+ centerVirtualIndex,
181
+ currentIndex,
182
+ data,
183
+ enableLoop,
184
+ isTriggerOpening,
185
+ windowSize,
186
+ });
140
187
 
141
188
  const constrainTranslation = useMemo(
142
189
  () => createBoundsConstraint({ height, width }),
143
190
  [width, height],
144
191
  );
145
192
 
146
- const scrollTo = useCallback(
147
- (index: number, animated: boolean) => {
148
- const scrollAction = createScrollAction(listRef.current, width + itemSpacing);
149
-
150
- return scrollAction.scrollTo(index, animated);
151
- },
152
- [width, itemSpacing],
153
- );
154
-
155
193
  const resetTransformState = useCallback(() => {
156
194
  translateX.set(withTiming(0));
157
195
  translateY.set(withTiming(0));
@@ -162,38 +200,179 @@ export const useGestureViewer = <ItemT, LC>({
162
200
  rotation.set(0);
163
201
  }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
164
202
 
165
- const syncPendingIndex = useCallback(
166
- (nextIndex: number) => {
167
- if (nextIndex < 0 || nextIndex >= dataLength) {
203
+ const resetTransformStateImmediately = useCallback(() => {
204
+ cancelAnimation(translateX);
205
+ cancelAnimation(translateY);
206
+ cancelAnimation(initialTranslateX);
207
+ cancelAnimation(initialTranslateY);
208
+ cancelAnimation(startScale);
209
+ cancelAnimation(scale);
210
+ cancelAnimation(rotation);
211
+ translateX.set(0);
212
+ translateY.set(0);
213
+ initialTranslateX.set(0);
214
+ initialTranslateY.set(0);
215
+ startScale.set(1);
216
+ scale.set(1);
217
+ rotation.set(0);
218
+ }, [initialTranslateX, initialTranslateY, rotation, scale, startScale, translateX, translateY]);
219
+
220
+ const commitCurrentIndex = useCallback((nextIndex: number) => {
221
+ pendingIndexRef.current = nextIndex;
222
+ currentIndexRef.current = nextIndex;
223
+ setCurrentIndex(nextIndex);
224
+ managerRef.current?.notifyStateChange();
225
+ }, []);
226
+
227
+ const commitVirtualIndexOnly = useCallback(
228
+ (nextVirtualIndex: number) => {
229
+ const nextLogicalIndex = getLogicalIndex(
230
+ nextVirtualIndex,
231
+ dataLengthRef.current,
232
+ enableLoopRef.current,
233
+ );
234
+
235
+ if (nextLogicalIndex === null) {
168
236
  return;
169
237
  }
170
238
 
171
- pendingIndexRef.current = nextIndex;
239
+ centerVirtualIndexRef.current = nextVirtualIndex;
240
+ setCenterVirtualIndex(nextVirtualIndex);
241
+ commitCurrentIndex(nextLogicalIndex);
172
242
  },
173
- [dataLength],
243
+ [commitCurrentIndex],
174
244
  );
175
245
 
176
- const syncCurrentIndex = useCallback(
177
- (nextIndex: number) => {
178
- if (!manager || nextIndex < 0 || nextIndex >= dataLength) {
246
+ const {
247
+ animateToVirtualPage,
248
+ horizontalPagingGesture,
249
+ isPageTransitioningRef,
250
+ pageTransitionLocked,
251
+ snapToVirtualPage,
252
+ visualPage,
253
+ } = useGestureViewerPaging({
254
+ centerVirtualIndex,
255
+ clearPendingWebSingleTap,
256
+ commitVirtualIndexOnly,
257
+ currentIndex,
258
+ dataLength,
259
+ enableLoop,
260
+ horizontalSwipeDistanceThresholdRatio,
261
+ horizontalSwipeEnabled,
262
+ horizontalSwipeVelocityThreshold,
263
+ initialPage: initialCurrentIndex,
264
+ isPinching,
265
+ isRotated,
266
+ isTriggerOpening,
267
+ isZoomed,
268
+ pageStride,
269
+ width,
270
+ });
271
+
272
+ const navigateToIndex = useCallback(
273
+ (targetIndex: number, options?: NavigationOptions, preferredDirection?: -1 | 1) => {
274
+ const resolution = resolveNavigation({
275
+ currentIndex: currentIndexRef.current,
276
+ dataLength: dataLengthRef.current,
277
+ enableLoop: enableLoopRef.current,
278
+ preferredDirection,
279
+ targetIndex,
280
+ });
281
+
282
+ if (resolution.kind === 'noop') {
283
+ return;
284
+ }
285
+
286
+ if (
287
+ isPageTransitioningRef.current &&
288
+ !shouldRunNavigationDuringTransition(resolution, options)
289
+ ) {
179
290
  return;
180
291
  }
181
292
 
182
- pendingIndexRef.current = nextIndex;
293
+ const targetVirtualIndex =
294
+ resolution.kind === 'step'
295
+ ? centerVirtualIndexRef.current + resolution.direction
296
+ : getVirtualIndexForLogicalIndex(
297
+ resolution.targetIndex,
298
+ centerVirtualIndexRef.current,
299
+ dataLengthRef.current,
300
+ enableLoopRef.current,
301
+ );
183
302
 
184
- const managerCurrentIndex = manager.getState().currentIndex;
303
+ if (targetVirtualIndex === null) {
304
+ return;
305
+ }
185
306
 
186
- if (nextIndex === managerCurrentIndex) {
307
+ if (resolution.kind !== 'step' || options?.animated === false) {
308
+ resetTransformStateImmediately();
309
+ snapToVirtualPage(targetVirtualIndex);
187
310
  return;
188
311
  }
189
312
 
190
- manager.setCurrentIndex(nextIndex);
191
- manager.notifyStateChange();
192
313
  resetTransformState();
314
+ animateToVirtualPage(targetVirtualIndex);
193
315
  },
194
- [dataLength, manager, resetTransformState],
316
+ [
317
+ animateToVirtualPage,
318
+ isPageTransitioningRef,
319
+ resetTransformStateImmediately,
320
+ resetTransformState,
321
+ snapToVirtualPage,
322
+ ],
195
323
  );
196
324
 
325
+ const navigateByDirection = useCallback(
326
+ (direction: -1 | 1) => {
327
+ const currentDataLength = dataLengthRef.current;
328
+
329
+ if (currentDataLength <= 0) {
330
+ return;
331
+ }
332
+
333
+ const nextIndex = currentIndexRef.current + direction;
334
+
335
+ if (nextIndex >= 0 && nextIndex < currentDataLength) {
336
+ navigateToIndex(nextIndex, undefined, direction);
337
+ return;
338
+ }
339
+
340
+ if (!enableLoopRef.current || currentDataLength <= 1) {
341
+ return;
342
+ }
343
+
344
+ navigateToIndex(direction === 1 ? 0 : currentDataLength - 1, undefined, direction);
345
+ },
346
+ [navigateToIndex],
347
+ );
348
+
349
+ const navigateToNext = useCallback(() => {
350
+ navigateByDirection(1);
351
+ }, [navigateByDirection]);
352
+
353
+ const navigateToPrevious = useCallback(() => {
354
+ navigateByDirection(-1);
355
+ }, [navigateByDirection]);
356
+
357
+ useGestureViewerManagerBridge({
358
+ currentIndexRef,
359
+ dataLengthRef,
360
+ goToIndex: navigateToIndex,
361
+ goToNext: navigateToNext,
362
+ goToPrevious: navigateToPrevious,
363
+ hasRotationChangeListeners,
364
+ hasZoomChangeListeners,
365
+ height,
366
+ id,
367
+ managerRef,
368
+ maxZoomScale,
369
+ rotation,
370
+ scale,
371
+ translateX,
372
+ translateY,
373
+ width,
374
+ });
375
+
197
376
  const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
198
377
  managerRef.current?.emitZoomChange(currentScale, prevScale);
199
378
  }, []);
@@ -202,146 +381,203 @@ export const useGestureViewer = <ItemT, LC>({
202
381
  managerRef.current?.emitRotationChange(currentRotation, prevRotation);
203
382
  }, []);
204
383
 
205
- const onAnimationComplete = useCallback(() => {
384
+ const finishTriggerOpening = useCallback(() => {
385
+ setIsTriggerOpening(false);
206
386
  onAnimationCompleteRef.current?.();
207
387
  }, []);
208
388
 
209
389
  useAnimatedReaction(
210
390
  () => scale.get(),
211
391
  (currentScale, previousScale) => {
212
- if (currentScale !== previousScale) {
392
+ if (currentScale !== previousScale && hasZoomChangeListeners.get()) {
213
393
  scheduleOnRN(emitZoomChange, currentScale, previousScale);
214
394
  }
215
395
 
216
- scheduleOnRN(setIsZoomed, currentScale > 1);
396
+ const currentIsZoomed = currentScale > 1;
397
+ const previousIsZoomed = (previousScale ?? 1) > 1;
398
+
399
+ if (previousScale === null || currentIsZoomed !== previousIsZoomed) {
400
+ scheduleOnRN(setIsZoomed, currentIsZoomed);
401
+ }
217
402
  },
218
403
  );
219
404
 
220
405
  useAnimatedReaction(
221
406
  () => rotation.get(),
222
407
  (currentRotation, previousRotation) => {
223
- if (currentRotation !== previousRotation) {
408
+ if (currentRotation !== previousRotation && hasRotationChangeListeners.get()) {
224
409
  scheduleOnRN(emitRotationChange, currentRotation, previousRotation);
225
410
  }
226
411
 
227
- scheduleOnRN(setIsRotated, currentRotation % 360 !== 0);
412
+ const currentIsRotated = currentRotation % 360 !== 0;
413
+ const previousIsRotated = (previousRotation ?? 0) % 360 !== 0;
414
+
415
+ if (previousRotation === null || currentIsRotated !== previousIsRotated) {
416
+ scheduleOnRN(setIsRotated, currentIsRotated);
417
+ }
228
418
  },
229
419
  );
230
420
 
231
421
  useEffect(() => {
232
- let unsubscribe: (() => void) | undefined;
422
+ return registry.subscribeToActiveTrigger(id, (node) => {
423
+ setActiveTriggerNode(node);
233
424
 
234
- const unsubscribeFromRegistry = registry.subscribeToManager(id, (managerInstance) => {
235
- setManager(managerInstance);
236
- unsubscribe = managerInstance?.subscribe((state) => {
237
- pendingIndexRef.current = state.currentIndex;
238
- setCurrentIndex(state.currentIndex);
239
- });
425
+ if (node) {
426
+ setIsTriggerOpening(true);
427
+ }
240
428
  });
241
-
242
- return () => {
243
- unsubscribeFromRegistry();
244
- unsubscribe?.();
245
- };
246
429
  }, [id]);
247
430
 
248
431
  useEffect(() => {
249
- return registry.subscribeToActiveTrigger(id, setActiveTriggerNode);
250
- }, [id]);
432
+ dataRef.current = data;
433
+ }, [data]);
251
434
 
252
435
  useEffect(() => {
253
- pendingIndexRef.current = initialIndex;
436
+ isZoomedRef.current = isZoomed;
437
+ }, [isZoomed]);
254
438
 
255
- if (!manager) {
256
- return;
439
+ useEffect(() => {
440
+ isRotatedRef.current = isRotated;
441
+ }, [isRotated]);
442
+
443
+ useEffect(() => {
444
+ isPinchingRef.current = isPinching;
445
+ }, [isPinching]);
446
+
447
+ useEffect(() => {
448
+ onSingleTapRef.current = onSingleTap;
449
+ }, [onSingleTap]);
450
+
451
+ useEffect(() => {
452
+ onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
453
+ }, [triggerAnimation?.onAnimationComplete]);
454
+
455
+ useLayoutEffect(() => {
456
+ const initialIndexChanged = previousInitialIndexRef.current !== initialIndex;
457
+ const shouldResolveInitialIndex =
458
+ initialIndexChanged || (!hasResolvedInitialIndexRef.current && dataLength > 0);
459
+
460
+ previousInitialIndexRef.current = initialIndex;
461
+ dataLengthRef.current = dataLength;
462
+ enableLoopRef.current = enableLoop;
463
+
464
+ hasResolvedInitialIndexRef.current = dataLength > 0;
465
+
466
+ const nextIndex = shouldResolveInitialIndex
467
+ ? clampIndex(initialIndex, dataLength)
468
+ : clampIndex(currentIndexRef.current, dataLength);
469
+
470
+ const nextVirtualIndex =
471
+ getVirtualIndexForLogicalIndex(
472
+ nextIndex,
473
+ centerVirtualIndexRef.current,
474
+ dataLength,
475
+ enableLoop,
476
+ ) ?? nextIndex;
477
+
478
+ const previousIndex = currentIndexRef.current;
479
+ const previousVirtualIndex = centerVirtualIndexRef.current;
480
+ const shouldSyncVirtualPage =
481
+ isPageTransitioningRef.current || previousVirtualIndex !== nextVirtualIndex;
482
+ const shouldResetTransform =
483
+ initialIndexChanged || previousIndex !== nextIndex || shouldSyncVirtualPage;
484
+
485
+ if (shouldSyncVirtualPage) {
486
+ snapToVirtualPage(nextVirtualIndex);
487
+
488
+ if (dataLength === 0) {
489
+ centerVirtualIndexRef.current = nextVirtualIndex;
490
+ setCenterVirtualIndex(nextVirtualIndex);
491
+ }
492
+ }
493
+
494
+ const shouldCommitIndexOutsidePaging = !shouldSyncVirtualPage || dataLength === 0;
495
+
496
+ if (shouldCommitIndexOutsidePaging && previousIndex !== nextIndex) {
497
+ commitCurrentIndex(nextIndex);
498
+ }
499
+
500
+ if (shouldCommitIndexOutsidePaging && previousIndex === nextIndex) {
501
+ managerRef.current?.notifyStateChange();
257
502
  }
258
503
 
259
- manager.setDataLength(dataLength);
260
- manager.setEnableHorizontalSwipe(enableHorizontalSwipe);
261
- manager.setCurrentIndex(initialIndex);
262
- manager.setWidth(width + itemSpacing);
263
- manager.setHeight(height);
264
- manager.setZoomSharedValues(scale, translateX, translateY, maxZoomScale);
265
- manager.setResetTransformCallback(resetTransformState);
266
- manager.setRotation(rotation);
267
- manager.setEnableLoop(enableLoop);
268
- manager.notifyStateChange();
504
+ if (shouldResetTransform) {
505
+ resetTransformState();
506
+ }
269
507
  }, [
508
+ commitCurrentIndex,
270
509
  dataLength,
271
- enableHorizontalSwipe,
272
- initialIndex,
273
- manager,
274
- width,
275
- itemSpacing,
276
- maxZoomScale,
277
510
  enableLoop,
278
- scale,
279
- height,
511
+ initialIndex,
512
+ isPageTransitioningRef,
280
513
  resetTransformState,
281
- translateX,
282
- translateY,
283
- rotation,
514
+ snapToVirtualPage,
284
515
  ]);
285
516
 
286
517
  useEffect(() => {
287
- if (!manager || !listRef.current) {
288
- return;
289
- }
290
-
291
- manager.setListRef(listRef.current);
292
- }, [manager]);
518
+ return () => {
519
+ triggerRectRef.current = null;
520
+ clearPendingWebSingleTap();
521
+ };
522
+ }, [clearPendingWebSingleTap]);
293
523
 
294
524
  useEffect(() => {
295
- const hasDataLengthChanged = previousDataLengthRef.current !== dataLength;
296
- const hasValidInitialIndex = initialIndex >= 0 && initialIndex < dataLength;
297
-
298
- previousDataLengthRef.current = dataLength;
299
- translateY.set(0);
300
- translateX.set(0);
301
- scale.set(1);
302
- backdropOpacity.set(1);
303
- startScale.set(1);
304
- rotation.set(0);
305
-
306
525
  if (
307
- !hasValidInitialIndex ||
308
- (!hasDataLengthChanged && adjustedInitialIndex <= 0) ||
309
- !listRef.current
526
+ !autoPlay ||
527
+ dataLength <= 1 ||
528
+ isTriggerOpening ||
529
+ isZoomed ||
530
+ isRotated ||
531
+ isPinching ||
532
+ (!enableLoop && currentIndex >= dataLength - 1)
310
533
  ) {
311
534
  return;
312
535
  }
313
536
 
314
- return scheduleInitialScroll(() => {
315
- scrollTo(adjustedInitialIndex, false);
316
- });
317
- }, [
318
- adjustedInitialIndex,
319
- dataLength,
320
- initialIndex,
321
- translateY,
322
- backdropOpacity,
323
- translateX,
324
- scale,
325
- startScale,
326
- rotation,
327
- scrollTo,
328
- ]);
537
+ const interval = Math.max(250, Math.floor(autoPlayInterval || 0));
329
538
 
330
- useEffect(() => {
331
- onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
332
- }, [triggerAnimation?.onAnimationComplete]);
539
+ if (!Number.isFinite(interval)) {
540
+ return;
541
+ }
333
542
 
334
- useEffect(() => {
335
- dataRef.current = data;
336
- }, [data]);
543
+ const timer = setInterval(() => {
544
+ if (
545
+ isPageTransitioningRef.current ||
546
+ isZoomedRef.current ||
547
+ isRotatedRef.current ||
548
+ isPinchingRef.current
549
+ ) {
550
+ return;
551
+ }
337
552
 
338
- useEffect(() => {
339
- managerRef.current = manager;
340
- }, [manager]);
553
+ const nextIndex =
554
+ currentIndexRef.current >= dataLengthRef.current - 1
555
+ ? enableLoopRef.current
556
+ ? 0
557
+ : null
558
+ : currentIndexRef.current + 1;
341
559
 
342
- useEffect(() => {
343
- onSingleTapRef.current = onSingleTap;
344
- }, [onSingleTap]);
560
+ if (nextIndex === null) {
561
+ return;
562
+ }
563
+
564
+ navigateToIndex(nextIndex);
565
+ }, interval);
566
+
567
+ return () => clearInterval(timer);
568
+ }, [
569
+ autoPlay,
570
+ autoPlayInterval,
571
+ currentIndex,
572
+ dataLength,
573
+ enableLoop,
574
+ isPinching,
575
+ isRotated,
576
+ isTriggerOpening,
577
+ isZoomed,
578
+ isPageTransitioningRef,
579
+ navigateToIndex,
580
+ ]);
345
581
 
346
582
  useEffect(() => {
347
583
  if (shouldStartTriggerAnimation && triggerRectRef.current) {
@@ -360,7 +596,7 @@ export const useGestureViewer = <ItemT, LC>({
360
596
  triggerScale.set(
361
597
  withTiming(1, animationConfig, (finished) => {
362
598
  if (finished) {
363
- scheduleOnRN(onAnimationComplete);
599
+ scheduleOnRN(finishTriggerOpening);
364
600
  }
365
601
  }),
366
602
  );
@@ -385,11 +621,18 @@ export const useGestureViewer = <ItemT, LC>({
385
621
  triggerScale,
386
622
  triggerTranslateX,
387
623
  triggerTranslateY,
388
- onAnimationComplete,
624
+ finishTriggerOpening,
389
625
  ]);
390
626
 
391
627
  useEffect(() => {
392
- if (!activeTriggerNode || typeof activeTriggerNode.measure !== 'function') {
628
+ if (!activeTriggerNode) {
629
+ return;
630
+ }
631
+
632
+ if (typeof activeTriggerNode.measure !== 'function') {
633
+ setIsTriggerOpening(false);
634
+ triggerOpacity.set(1);
635
+ registry.clearActiveTriggerNode(id);
393
636
  return;
394
637
  }
395
638
 
@@ -402,6 +645,8 @@ export const useGestureViewer = <ItemT, LC>({
402
645
  } satisfies TriggerRect;
403
646
 
404
647
  if (!isValidTriggerRect(nextTriggerRect)) {
648
+ setIsTriggerOpening(false);
649
+ triggerOpacity.set(1);
405
650
  registry.clearActiveTriggerNode(id);
406
651
  return;
407
652
  }
@@ -411,13 +656,7 @@ export const useGestureViewer = <ItemT, LC>({
411
656
  setShouldStartTriggerAnimation(true);
412
657
  registry.clearActiveTriggerNode(id);
413
658
  });
414
- }, [activeTriggerNode, id, isValidTriggerRect, triggerOpacity]);
415
-
416
- useEffect(() => {
417
- return () => {
418
- triggerRectRef.current = null;
419
- };
420
- }, []);
659
+ }, [activeTriggerNode, id, triggerOpacity]);
421
660
 
422
661
  const animateDismissToRect = useCallback(
423
662
  (rect: TriggerRect) => {
@@ -455,6 +694,10 @@ export const useGestureViewer = <ItemT, LC>({
455
694
  }, [onDismiss]);
456
695
 
457
696
  const handleDismiss = useCallback(() => {
697
+ if (isPageTransitioningRef.current) {
698
+ return;
699
+ }
700
+
458
701
  onDismissStart?.();
459
702
 
460
703
  const dismissTargetIndex = pendingIndexRef.current;
@@ -490,10 +733,10 @@ export const useGestureViewer = <ItemT, LC>({
490
733
  }
491
734
 
492
735
  dismissWithoutTrigger();
493
- }, [animateDismissToRect, dismissWithoutTrigger, id, isValidTriggerRect, onDismissStart]);
736
+ }, [animateDismissToRect, dismissWithoutTrigger, id, isPageTransitioningRef, onDismissStart]);
494
737
 
495
738
  const dismissGesture = useMemo(() => {
496
- const canDismiss = !isZoomed && dismissOptions.enabled;
739
+ const canDismiss = !isTriggerOpening && !isZoomed && dismissOptions.enabled;
497
740
 
498
741
  return Gesture.Pan()
499
742
  .minDistance(10)
@@ -504,9 +747,18 @@ export const useGestureViewer = <ItemT, LC>({
504
747
  .withRef(dismissGestureRef)
505
748
  .enabled(canDismiss)
506
749
  .onUpdate((event) => {
750
+ if (pageTransitionLocked.get()) {
751
+ return;
752
+ }
753
+
507
754
  translateY.set(event.translationY / dismissOptions.resistance);
508
755
  })
509
756
  .onEnd((event) => {
757
+ if (pageTransitionLocked.get()) {
758
+ translateY.set(withSpring(0, PAGE_SPRING_CONFIG));
759
+ return;
760
+ }
761
+
510
762
  if (
511
763
  canDismiss &&
512
764
  shouldDismissByDirection(
@@ -519,28 +771,120 @@ export const useGestureViewer = <ItemT, LC>({
519
771
  return;
520
772
  }
521
773
 
522
- translateY.set(
523
- withSpring(0, {
524
- damping: 50,
525
- energyThreshold: 6e-9,
526
- mass: 4,
527
- overshootClamping: false,
528
- stiffness: 600,
529
- }),
530
- );
774
+ translateY.set(withSpring(0, PAGE_SPRING_CONFIG));
531
775
  });
532
- }, [translateY, dismissOptions, handleDismiss, isZoomed]);
776
+ }, [translateY, dismissOptions, handleDismiss, isTriggerOpening, isZoomed, pageTransitionLocked]);
777
+
778
+ const getCurrentTapTarget = useCallback((): { index: number; item: ItemT } | null => {
779
+ const index = pendingIndexRef.current;
780
+
781
+ if (index < 0 || index >= dataRef.current.length || !(index in dataRef.current)) {
782
+ return null;
783
+ }
784
+
785
+ return { index, item: dataRef.current[index] as ItemT };
786
+ }, []);
787
+
788
+ const isWebInteractionLocked = useCallback(
789
+ () => isPageTransitioningRef.current || pageTransitionLocked.get(),
790
+ [isPageTransitioningRef, pageTransitionLocked],
791
+ );
792
+
793
+ const emitSingleTap = useCallback<EmitSingleTap<ItemT>>(
794
+ (x, y, tapTarget) => {
795
+ if (isPageTransitioningRef.current) {
796
+ return;
797
+ }
798
+
799
+ const target = tapTarget ?? getCurrentTapTarget();
800
+
801
+ if (!target) {
802
+ return;
803
+ }
804
+
805
+ const { index: targetIndex, item: targetItem } = target;
806
+
807
+ if (targetIndex < 0) {
808
+ return;
809
+ }
810
+
811
+ managerRef.current?.emitTap({ kind: 'single', x, y, index: targetIndex });
812
+ onSingleTapRef.current?.({ x, y, index: targetIndex, item: targetItem });
813
+ },
814
+ [getCurrentTapTarget, isPageTransitioningRef],
815
+ );
816
+
817
+ const singleTapGesture = useMemo(
818
+ () =>
819
+ Gesture.Tap()
820
+ .enabled(enableNativeTapGestures)
821
+ .numberOfTaps(1)
822
+ .onEnd((event, success) => {
823
+ if (!success || pageTransitionLocked.get()) {
824
+ return;
825
+ }
826
+
827
+ scheduleOnRN(emitSingleTap, event.x, event.y);
828
+ }),
829
+ [emitSingleTap, pageTransitionLocked],
830
+ );
831
+
832
+ const doubleTapGesture = useMemo(
833
+ () =>
834
+ Gesture.Tap()
835
+ .enabled(enableDoubleTapZoom && enableNativeTapGestures)
836
+ .numberOfTaps(2)
837
+ .onEnd((event) => {
838
+ if (pageTransitionLocked.get()) {
839
+ return;
840
+ }
841
+
842
+ applyTapZoomAtPoint({
843
+ x: event.x,
844
+ y: event.y,
845
+ width,
846
+ height,
847
+ maxZoomScale,
848
+ scale,
849
+ translateX,
850
+ translateY,
851
+ });
852
+ }),
853
+ [
854
+ enableDoubleTapZoom,
855
+ height,
856
+ maxZoomScale,
857
+ pageTransitionLocked,
858
+ scale,
859
+ translateX,
860
+ translateY,
861
+ width,
862
+ ],
863
+ );
864
+
865
+ const tapGesture = useMemo(
866
+ () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
867
+ [doubleTapGesture, singleTapGesture],
868
+ );
533
869
 
534
870
  const zoomPinchGesture = useMemo(
535
871
  () =>
536
872
  Gesture.Pinch()
537
873
  .enabled(enablePinchZoom)
538
874
  .onTouchesDown((event) => {
875
+ if (pageTransitionLocked.get()) {
876
+ return;
877
+ }
878
+
539
879
  if (event.numberOfTouches === 2) {
540
880
  scheduleOnRN(setIsPinching, true);
541
881
  }
542
882
  })
543
883
  .onStart((event) => {
884
+ if (pageTransitionLocked.get()) {
885
+ return;
886
+ }
887
+
544
888
  startScale.set(scale.get());
545
889
  initialTranslateX.set(translateX.get());
546
890
  initialTranslateY.set(translateY.get());
@@ -551,6 +895,10 @@ export const useGestureViewer = <ItemT, LC>({
551
895
  hasActiveFocal.set(false);
552
896
  })
553
897
  .onUpdate((event) => {
898
+ if (pageTransitionLocked.get()) {
899
+ return;
900
+ }
901
+
554
902
  const initialScale = startScale.get();
555
903
  const newScale = initialScale * event.scale;
556
904
 
@@ -612,6 +960,10 @@ export const useGestureViewer = <ItemT, LC>({
612
960
  translateY.set(constrainedTranslateY);
613
961
  })
614
962
  .onEnd(() => {
963
+ if (pageTransitionLocked.get()) {
964
+ return;
965
+ }
966
+
615
967
  const currentScale = scale.get();
616
968
 
617
969
  if (currentScale > maxZoomScale) {
@@ -684,6 +1036,7 @@ export const useGestureViewer = <ItemT, LC>({
684
1036
  startFocalX,
685
1037
  startFocalY,
686
1038
  hasActiveFocal,
1039
+ pageTransitionLocked,
687
1040
  ],
688
1041
  );
689
1042
 
@@ -694,10 +1047,18 @@ export const useGestureViewer = <ItemT, LC>({
694
1047
  .activeCursor('grabbing')
695
1048
  .averageTouches(true)
696
1049
  .onBegin(() => {
1050
+ if (pageTransitionLocked.get()) {
1051
+ return;
1052
+ }
1053
+
697
1054
  initialTranslateX.set(translateX.get());
698
1055
  initialTranslateY.set(translateY.get());
699
1056
  })
700
1057
  .onUpdate((event) => {
1058
+ if (pageTransitionLocked.get()) {
1059
+ return;
1060
+ }
1061
+
701
1062
  const currentScale = scale.get();
702
1063
 
703
1064
  if (currentScale > 1) {
@@ -724,73 +1085,30 @@ export const useGestureViewer = <ItemT, LC>({
724
1085
  initialTranslateX,
725
1086
  initialTranslateY,
726
1087
  constrainTranslation,
1088
+ pageTransitionLocked,
727
1089
  ],
728
1090
  );
729
1091
 
730
- const emitSingleTap = useCallback((x: number, y: number) => {
731
- const index = pendingIndexRef.current;
732
- const currentData = dataRef.current;
733
-
734
- if (index < 0 || index >= currentData.length) {
735
- return;
736
- }
737
-
738
- managerRef.current?.emitTap({ kind: 'single', x, y, index });
739
-
740
- const item = currentData[index];
741
-
742
- if (item === undefined) {
743
- return;
744
- }
745
-
746
- onSingleTapRef.current?.({ x, y, index, item });
747
- }, []);
748
-
749
- const singleTapGesture = useMemo(
750
- () =>
751
- Gesture.Tap()
752
- .enabled(Platform.OS !== 'web')
753
- .numberOfTaps(1)
754
- .onEnd((event, success) => {
755
- if (!success) {
756
- return;
757
- }
758
-
759
- scheduleOnRN(emitSingleTap, event.x, event.y);
760
- }),
761
- [emitSingleTap],
762
- );
763
-
764
- const doubleTapGesture = useMemo(
765
- () =>
766
- Gesture.Tap()
767
- .enabled(enableDoubleTapZoom && Platform.OS !== 'web')
768
- .numberOfTaps(2)
769
- .onEnd((event) => {
770
- applyTapZoomAtPoint({
771
- x: event.x,
772
- y: event.y,
773
- width,
774
- height,
775
- maxZoomScale,
776
- scale,
777
- translateX,
778
- translateY,
779
- });
780
- }),
781
- [enableDoubleTapZoom, height, maxZoomScale, scale, translateX, translateY, width],
782
- );
783
-
784
- const tapGesture = useMemo(
785
- () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
786
- [doubleTapGesture, singleTapGesture],
787
- );
788
-
789
1092
  const zoomGesture = useMemo(
790
1093
  () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, tapGesture)),
791
1094
  [zoomPinchGesture, zoomPanGesture, tapGesture],
792
1095
  );
793
1096
 
1097
+ const onWebClick = useWebClickHandler({
1098
+ clearPendingWebSingleTap,
1099
+ emitSingleTap,
1100
+ enableDoubleTapZoom,
1101
+ getCurrentTapTarget,
1102
+ height,
1103
+ maxZoomScale,
1104
+ scale,
1105
+ scheduleWebSingleTap,
1106
+ isInteractionLocked: isWebInteractionLocked,
1107
+ translateX,
1108
+ translateY,
1109
+ width,
1110
+ });
1111
+
794
1112
  const animatedStyle = useAnimatedStyle(() => ({
795
1113
  opacity: triggerOpacity.get(),
796
1114
  transform: [
@@ -818,54 +1136,22 @@ export const useGestureViewer = <ItemT, LC>({
818
1136
  return { opacity: baseOpacity * dismissOpacity };
819
1137
  }, [dismissOptions.direction, dismissOptions.fadeBackdrop]);
820
1138
 
821
- const nativeScrollGesture = useMemo(() => {
822
- return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
823
- }, []);
824
-
825
- const { activeListIndex, onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } =
826
- useGestureViewerPaging({
827
- adjustedInitialIndex,
828
- autoPlay,
829
- autoPlayInterval,
830
- currentIndex,
831
- dataLength,
832
- enableDoubleTapZoom,
833
- enableHorizontalSwipe,
834
- enableLoop,
835
- height,
836
- isRotated,
837
- isZoomed,
838
- itemSpacing,
839
- manager,
840
- maxZoomScale,
841
- onSingleTap: emitSingleTap,
842
- scale,
843
- scrollTo,
844
- syncCurrentIndex,
845
- syncPendingIndex,
846
- translateX,
847
- translateY,
848
- width,
849
- });
850
-
851
1139
  return {
852
- activeListIndex,
853
1140
  animatedStyle,
854
1141
  backdropStyle,
1142
+ centerVirtualIndex: renderCenterVirtualIndex,
1143
+ currentIndex: renderCurrentIndex,
855
1144
  dataLength,
856
1145
  dismissGesture,
857
1146
  handleDismiss,
1147
+ horizontalPagingGesture,
858
1148
  isPinching,
859
1149
  isRotated,
860
-
861
1150
  isZoomed,
862
- listRef,
863
- nativeScrollGesture,
864
1151
  onWebClick,
865
- onMomentumScrollEnd,
866
- onScroll,
867
-
868
- onScrollBeginDrag,
1152
+ pageStride,
1153
+ renderWindowSlots,
1154
+ visualPage,
869
1155
  zoomGesture,
870
1156
  };
871
1157
  };