rn-pdf-king 0.1.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 (84) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +148 -0
  3. package/android/build.gradle +55 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/expo/modules/rnpdfking/PdfKing.kt +693 -0
  6. package/android/src/main/java/expo/modules/rnpdfking/RnPdfKingModule.kt +163 -0
  7. package/android/src/main/java/expo/modules/rnpdfking/RnPdfKingView.kt +184 -0
  8. package/build/PdfDocument.d.ts +19 -0
  9. package/build/PdfDocument.d.ts.map +1 -0
  10. package/build/PdfDocument.js +81 -0
  11. package/build/PdfDocument.js.map +1 -0
  12. package/build/PdfPage.d.ts +24 -0
  13. package/build/PdfPage.d.ts.map +1 -0
  14. package/build/PdfPage.js +13 -0
  15. package/build/PdfPage.js.map +1 -0
  16. package/build/RnPdfKing.types.d.ts +48 -0
  17. package/build/RnPdfKing.types.d.ts.map +1 -0
  18. package/build/RnPdfKing.types.js +2 -0
  19. package/build/RnPdfKing.types.js.map +1 -0
  20. package/build/RnPdfKingModule.d.ts +13 -0
  21. package/build/RnPdfKingModule.d.ts.map +1 -0
  22. package/build/RnPdfKingModule.js +4 -0
  23. package/build/RnPdfKingModule.js.map +1 -0
  24. package/build/RnPdfKingModule.web.d.ts +13 -0
  25. package/build/RnPdfKingModule.web.d.ts.map +1 -0
  26. package/build/RnPdfKingModule.web.js +21 -0
  27. package/build/RnPdfKingModule.web.js.map +1 -0
  28. package/build/RnPdfKingView.d.ts +4 -0
  29. package/build/RnPdfKingView.d.ts.map +1 -0
  30. package/build/RnPdfKingView.js +7 -0
  31. package/build/RnPdfKingView.js.map +1 -0
  32. package/build/RnPdfKingView.web.d.ts +4 -0
  33. package/build/RnPdfKingView.web.d.ts.map +1 -0
  34. package/build/RnPdfKingView.web.js +7 -0
  35. package/build/RnPdfKingView.web.js.map +1 -0
  36. package/build/ZoomableList.d.ts +37 -0
  37. package/build/ZoomableList.d.ts.map +1 -0
  38. package/build/ZoomableList.js +289 -0
  39. package/build/ZoomableList.js.map +1 -0
  40. package/build/ZoomablePage.d.ts +10 -0
  41. package/build/ZoomablePage.d.ts.map +1 -0
  42. package/build/ZoomablePage.js +15 -0
  43. package/build/ZoomablePage.js.map +1 -0
  44. package/build/ZoomablePdfPage.d.ts +10 -0
  45. package/build/ZoomablePdfPage.d.ts.map +1 -0
  46. package/build/ZoomablePdfPage.js +17 -0
  47. package/build/ZoomablePdfPage.js.map +1 -0
  48. package/build/index.d.ts +8 -0
  49. package/build/index.d.ts.map +1 -0
  50. package/build/index.js +10 -0
  51. package/build/index.js.map +1 -0
  52. package/build/zoom/constants.d.ts +36 -0
  53. package/build/zoom/constants.d.ts.map +1 -0
  54. package/build/zoom/constants.js +36 -0
  55. package/build/zoom/constants.js.map +1 -0
  56. package/build/zoom/index.d.ts +255 -0
  57. package/build/zoom/index.d.ts.map +1 -0
  58. package/build/zoom/index.js +783 -0
  59. package/build/zoom/index.js.map +1 -0
  60. package/build/zoom/utils.d.ts +55 -0
  61. package/build/zoom/utils.d.ts.map +1 -0
  62. package/build/zoom/utils.js +66 -0
  63. package/build/zoom/utils.js.map +1 -0
  64. package/bun.lock +2217 -0
  65. package/expo-module.config.json +9 -0
  66. package/ios/RnPdfKing.podspec +29 -0
  67. package/ios/RnPdfKingModule.swift +48 -0
  68. package/ios/RnPdfKingView.swift +38 -0
  69. package/package.json +45 -0
  70. package/src/PdfDocument.tsx +115 -0
  71. package/src/PdfPage.tsx +57 -0
  72. package/src/RnPdfKing.types.ts +32 -0
  73. package/src/RnPdfKingModule.ts +15 -0
  74. package/src/RnPdfKingModule.web.ts +24 -0
  75. package/src/RnPdfKingView.tsx +11 -0
  76. package/src/RnPdfKingView.web.tsx +15 -0
  77. package/src/ZoomableList.tsx +438 -0
  78. package/src/ZoomablePage.tsx +31 -0
  79. package/src/ZoomablePdfPage.tsx +34 -0
  80. package/src/index.ts +9 -0
  81. package/src/zoom/constants.ts +40 -0
  82. package/src/zoom/index.tsx +1267 -0
  83. package/src/zoom/utils.ts +96 -0
  84. package/tsconfig.json +9 -0
@@ -0,0 +1,438 @@
1
+ import React, {
2
+ useMemo,
3
+ useRef,
4
+ useState,
5
+ createContext,
6
+ useContext,
7
+ } from "react";
8
+ import {
9
+ StyleProp,
10
+ ViewStyle,
11
+ View,
12
+ LayoutChangeEvent,
13
+ StyleSheet,
14
+ Text,
15
+ } from "react-native";
16
+ import { GestureDetector, Gesture } from "react-native-gesture-handler";
17
+ import Animated, {
18
+ runOnJS,
19
+ useAnimatedRef,
20
+ useAnimatedStyle,
21
+ useDerivedValue,
22
+ useSharedValue,
23
+ scrollTo,
24
+ withTiming,
25
+ withDelay,
26
+ } from "react-native-reanimated";
27
+ import { useZoomGesture, UseZoomGestureProps } from "./zoom/index";
28
+ import {
29
+ FlashList,
30
+ FlashListProps,
31
+ ListRenderItemInfo,
32
+ } from "@shopify/flash-list";
33
+
34
+ const AnimatedFlashList = Animated.createAnimatedComponent(FlashList as any) as any;
35
+
36
+ const PAGE_SLIDER_PADDING_Y = 16;
37
+ const PAGE_SLIDER_THUMB_SIZE = 28;
38
+
39
+ // Context for ZoomableList state
40
+ interface ZoomableListContextType {
41
+ isZoomed: boolean;
42
+ isPanning: boolean;
43
+ isPinching: boolean;
44
+ width: number;
45
+ }
46
+
47
+ const ZoomableListContext = createContext<ZoomableListContextType | undefined>(
48
+ undefined,
49
+ );
50
+
51
+ export const useZoomableList = () => {
52
+ const context = useContext(ZoomableListContext);
53
+ if (!context) {
54
+ throw new Error("useZoomableList must be used within a ZoomableList");
55
+ }
56
+ return context;
57
+ };
58
+
59
+ export interface ZoomableListProps<T> extends Omit<
60
+ FlashListProps<T>,
61
+ "renderItem"
62
+ > {
63
+ renderItem: (
64
+ info: ListRenderItemInfo<T> & { width: number },
65
+ ) => React.ReactElement | null;
66
+ style?: StyleProp<ViewStyle>;
67
+ zoomProps?: Omit<UseZoomGestureProps, "parentAnimatedScrollRef">;
68
+ onZoomChange?: (scale: number) => void;
69
+ onZoomStateChange?: (isZoomed: boolean) => void;
70
+ /**
71
+ * Google Drive-style page scrubber on the right side.
72
+ * Works best for "page" lists where each item is a page.
73
+ */
74
+ pageSliderEnabled?: boolean;
75
+ /**
76
+ * Label renderer for the bubble. Defaults to "current/total".
77
+ * current is 1-based.
78
+ */
79
+ pageSliderLabel?: (current: number, total: number) => string;
80
+ /**
81
+ * Logo/Icon for the slider thumb.
82
+ */
83
+ pageSliderLogo?: React.ReactNode;
84
+ }
85
+
86
+ export function ZoomableList<T>(props: ZoomableListProps<T>) {
87
+ const {
88
+ style,
89
+ zoomProps,
90
+ onZoomChange,
91
+ onZoomStateChange,
92
+ renderItem,
93
+ pageSliderEnabled = false,
94
+ pageSliderLabel,
95
+ pageSliderLogo,
96
+ ...flashListProps
97
+ } = props;
98
+
99
+ const [width, setWidth] = useState(0);
100
+ const [isZoomed, setIsZoomed] = useState(false);
101
+ const [isPanning, setIsPanning] = useState(false);
102
+ const [isPinching, setIsPinching] = useState(false);
103
+ const [currentIndex, setCurrentIndex] = useState(0);
104
+ const [isScrubbing, setIsScrubbing] = useState(false);
105
+ const [scrubIndex, setScrubIndex] = useState(0);
106
+ const [trackHeight, setTrackHeight] = useState(0);
107
+ const [hasScrolled, setHasScrolled] = useState(false);
108
+
109
+ // @ts-ignore
110
+ const listRef = useAnimatedRef<FlashList<T>>();
111
+
112
+ const {
113
+ zoomGesture,
114
+ contentContainerAnimatedStyle,
115
+ onLayout,
116
+ onLayoutContent,
117
+ onScroll,
118
+ } = useZoomGesture({
119
+ disableVerticalPan: true,
120
+ parentAnimatedScrollRef: listRef,
121
+ ...zoomProps,
122
+ onPanningStarted: () => {
123
+ setIsPanning(true);
124
+ zoomProps?.onPanningStarted?.();
125
+ },
126
+ onPanningEnd: () => {
127
+ setIsPanning(false);
128
+ zoomProps?.onPanningEnd?.();
129
+ },
130
+ onPinchingStarted: () => {
131
+ setIsPinching(true);
132
+ zoomProps?.onPinchingStarted?.();
133
+ },
134
+ onPinchingStopped: () => {
135
+ setIsPinching(false);
136
+ zoomProps?.onPinchingStopped?.();
137
+ },
138
+ onZoomChange: (s: number) => {
139
+ const newIsZoomed = s > 1.05; // Small buffer
140
+ if (newIsZoomed !== isZoomed) {
141
+ setIsZoomed(newIsZoomed);
142
+ onZoomStateChange?.(newIsZoomed);
143
+ }
144
+ onZoomChange?.(s);
145
+ },
146
+ });
147
+
148
+ const handleLayout = (e: LayoutChangeEvent) => {
149
+ if (e.nativeEvent.layout.width !== width) {
150
+ setWidth(e.nativeEvent.layout.width);
151
+ }
152
+ };
153
+
154
+ const totalCount = flashListProps.data?.length ?? 0;
155
+ const sliderEnabled = pageSliderEnabled && totalCount > 1;
156
+ const bubbleText = (
157
+ pageSliderLabel ?? ((cur: number, total: number) => `${cur}/${total}`)
158
+ )((isScrubbing ? scrubIndex : currentIndex) + 1, totalCount);
159
+
160
+ const onViewableItemsChanged = useRef(
161
+ ({ viewableItems }: { viewableItems: Array<{ index: number | null }> }) => {
162
+ const first = viewableItems?.find(
163
+ (v) => typeof v.index === "number" && v.index !== null,
164
+ );
165
+ if (first?.index != null) setCurrentIndex(first.index);
166
+ },
167
+ ).current;
168
+
169
+ const viewabilityConfig = useMemo(
170
+ () => ({
171
+ itemVisiblePercentThreshold: 55,
172
+ minimumViewTime: 60,
173
+ }),
174
+ [],
175
+ );
176
+
177
+ const thumbY = useSharedValue(0);
178
+ const bubbleOpacity = useSharedValue(0);
179
+ const contentHeight = useSharedValue(0);
180
+ const viewportHeight = useSharedValue(0);
181
+
182
+ const handleScroll = (e: any) => {
183
+ // Keep zoom gesture bookkeeping intact
184
+ onScroll?.(e);
185
+ // Preserve consumer scroll handler if provided
186
+ (flashListProps as any).onScroll?.(e);
187
+
188
+ const ne = e?.nativeEvent;
189
+ if (ne) {
190
+ contentHeight.value = ne.contentSize?.height ?? 0;
191
+ viewportHeight.value = ne.layoutMeasurement?.height ?? 0;
192
+ }
193
+
194
+ if (!sliderEnabled || trackHeight <= 0) return;
195
+
196
+ if (!isScrubbing) {
197
+ // Show bubble while panning/scrolling and hide after 1s of inactivity
198
+ bubbleOpacity.value = 1;
199
+ bubbleOpacity.value = withDelay(1000, withTiming(0, { duration: 300 }));
200
+ }
201
+
202
+ const offsetY = ne?.contentOffset?.y ?? 0;
203
+ const contentH = ne?.contentSize?.height ?? 0;
204
+ const viewportH = ne?.layoutMeasurement?.height ?? 0;
205
+ const scrollable = Math.max(1, contentH - viewportH);
206
+ const ratio = Math.max(0, Math.min(1, offsetY / scrollable));
207
+ thumbY.value = ratio * trackHeight;
208
+ if (!hasScrolled) setHasScrolled(true);
209
+ };
210
+
211
+ useDerivedValue(() => {
212
+ if (
213
+ !sliderEnabled ||
214
+ isScrubbing ||
215
+ hasScrolled ||
216
+ trackHeight <= 0 ||
217
+ totalCount <= 1
218
+ )
219
+ return;
220
+ const ratio = currentIndex / (totalCount - 1);
221
+ thumbY.value = ratio * trackHeight;
222
+ }, [
223
+ currentIndex,
224
+ isScrubbing,
225
+ sliderEnabled,
226
+ totalCount,
227
+ trackHeight,
228
+ hasScrolled,
229
+ ]);
230
+
231
+ const updateScrubFromY = (y: number) => {
232
+ if (!sliderEnabled || trackHeight <= 0) return;
233
+ // Gesture y is relative to the track view; normalize into usable range.
234
+ const normalized = y - PAGE_SLIDER_PADDING_Y;
235
+ const clamped = Math.max(0, Math.min(trackHeight, normalized));
236
+ const ratio = trackHeight > 0 ? clamped / trackHeight : 0;
237
+ const idx = Math.max(
238
+ 0,
239
+ Math.min(totalCount - 1, Math.round(ratio * (totalCount - 1))),
240
+ );
241
+ setScrubIndex(idx);
242
+ };
243
+
244
+ const panGesture = useMemo(() => {
245
+ return Gesture.Pan()
246
+ .manualActivation(true)
247
+ .onTouchesDown((_e, manager) => {
248
+ manager.activate();
249
+ })
250
+ .enabled(sliderEnabled && !isZoomed && !isPanning && !isPinching)
251
+ .onBegin(() => {
252
+ bubbleOpacity.value = 1;
253
+ runOnJS(setIsScrubbing)(true);
254
+ })
255
+ .onUpdate((e) => {
256
+ // keep thumbY in the same normalized coordinate space as trackHeight
257
+ const y = Math.max(
258
+ 0,
259
+ Math.min(trackHeight, e.y - PAGE_SLIDER_PADDING_Y),
260
+ );
261
+ thumbY.value = y;
262
+
263
+ if (trackHeight > 0) {
264
+ const ratio = y / trackHeight;
265
+ const scrollableHeight = contentHeight.value - viewportHeight.value;
266
+ if (scrollableHeight > 0) {
267
+ scrollTo(listRef, 0, ratio * scrollableHeight, true);
268
+ }
269
+ }
270
+
271
+ runOnJS(updateScrubFromY)(e.y);
272
+ })
273
+ .onEnd(() => {
274
+ bubbleOpacity.value = withTiming(0, { duration: 300 });
275
+ runOnJS(setIsScrubbing)(false);
276
+ })
277
+ .onFinalize(() => {
278
+ bubbleOpacity.value = withTiming(0, { duration: 300 });
279
+ runOnJS(setIsScrubbing)(false);
280
+ });
281
+ }, [
282
+ bubbleOpacity,
283
+ isPanning,
284
+ isPinching,
285
+ isZoomed,
286
+ sliderEnabled,
287
+ thumbY,
288
+ trackHeight,
289
+ totalCount,
290
+ contentHeight,
291
+ viewportHeight,
292
+ listRef,
293
+ ]);
294
+
295
+ const thumbStyle = useAnimatedStyle(() => {
296
+ const y =
297
+ trackHeight > 0 ? Math.max(0, Math.min(trackHeight, thumbY.value)) : 0;
298
+ return {
299
+ transform: [{ translateY: y + PAGE_SLIDER_PADDING_Y }],
300
+ };
301
+ }, [trackHeight]);
302
+
303
+ const bubbleStyle = useAnimatedStyle(() => {
304
+ const y =
305
+ trackHeight > 0 ? Math.max(0, Math.min(trackHeight, thumbY.value)) : 0;
306
+ return {
307
+ opacity: bubbleOpacity.value,
308
+ transform: [{ translateY: y + PAGE_SLIDER_PADDING_Y }],
309
+ };
310
+ });
311
+
312
+ return (
313
+ <ZoomableListContext.Provider
314
+ value={{ isZoomed, isPanning, isPinching, width }}
315
+ >
316
+ <View style={[styles.container, style]} onLayout={handleLayout}>
317
+ <GestureDetector gesture={zoomGesture}>
318
+ <View style={{ flex: 1 }} onLayout={onLayout} collapsable={false}>
319
+ <Animated.View
320
+ style={[contentContainerAnimatedStyle, { flex: 1 }]}
321
+ onLayout={onLayoutContent}
322
+ >
323
+ <AnimatedFlashList
324
+ ref={listRef}
325
+ scrollEnabled={!isZoomed && !isPanning && !isPinching}
326
+ scrollEventThrottle={16}
327
+ renderItem={(info: ListRenderItemInfo<T>) => renderItem({ ...info, width })}
328
+ {...flashListProps}
329
+ onScroll={handleScroll}
330
+ onViewableItemsChanged={
331
+ sliderEnabled
332
+ ? (onViewableItemsChanged as any)
333
+ : flashListProps.onViewableItemsChanged
334
+ }
335
+ viewabilityConfig={
336
+ sliderEnabled
337
+ ? (viewabilityConfig as any)
338
+ : flashListProps.viewabilityConfig
339
+ }
340
+ />
341
+ </Animated.View>
342
+ </View>
343
+ </GestureDetector>
344
+
345
+ {sliderEnabled ? (
346
+ <View pointerEvents="box-none" style={styles.pageSliderOverlay}>
347
+ <View style={styles.pageSliderGestureContainer}>
348
+ <GestureDetector gesture={panGesture}>
349
+ <View
350
+ style={styles.pageSliderTrack}
351
+ collapsable={false}
352
+ onLayout={(e) => {
353
+ const h = e.nativeEvent.layout.height;
354
+ const usable = Math.max(
355
+ 0,
356
+ h - PAGE_SLIDER_PADDING_Y * 2 - PAGE_SLIDER_THUMB_SIZE,
357
+ );
358
+ setTrackHeight(usable);
359
+ }}
360
+ >
361
+ <Animated.View style={[styles.pageSliderBubble, bubbleStyle]}>
362
+ <Text style={styles.pageSliderBubbleText}>
363
+ {bubbleText}
364
+ </Text>
365
+ </Animated.View>
366
+ <Animated.View style={[styles.pageSliderThumb, thumbStyle]}>
367
+ {pageSliderLogo}
368
+ </Animated.View>
369
+ </View>
370
+ </GestureDetector>
371
+ </View>
372
+ </View>
373
+ ) : null}
374
+ </View>
375
+ </ZoomableListContext.Provider>
376
+ );
377
+ }
378
+
379
+ const styles = StyleSheet.create({
380
+ container: {
381
+ flex: 1,
382
+ overflow: "hidden",
383
+ },
384
+ pageSliderOverlay: {
385
+ position: "absolute",
386
+ right: 0,
387
+ top: 0,
388
+ bottom: 0,
389
+ width: 10,
390
+ alignItems: "flex-end",
391
+ justifyContent: "flex-start",
392
+ },
393
+ pageSliderGestureContainer: {
394
+ flex: 1,
395
+ alignSelf: "stretch",
396
+ alignItems: "flex-end",
397
+ },
398
+ pageSliderTrack: {
399
+ flex: 1,
400
+ width: 10,
401
+ alignItems: "flex-end",
402
+ justifyContent: "flex-start",
403
+ paddingVertical: 16,
404
+ },
405
+ pageSliderThumb: {
406
+ position: "absolute",
407
+ right: 2,
408
+ width: 38,
409
+ height: 38,
410
+ borderRadius: 999,
411
+ backgroundColor: "rgba(255,255,255,0.92)",
412
+ borderWidth: 1,
413
+ borderColor: "rgba(0,0,0,0.12)",
414
+ shadowColor: "#000",
415
+ shadowOpacity: 0.15,
416
+ shadowRadius: 6,
417
+ shadowOffset: { width: 0, height: 2 },
418
+ elevation: 3,
419
+ alignItems: "center",
420
+ justifyContent: "center",
421
+ },
422
+ pageSliderBubble: {
423
+ position: "absolute",
424
+ right: 38,
425
+ minWidth: 80,
426
+ paddingHorizontal: 8,
427
+ paddingVertical: 10,
428
+ borderRadius: 999,
429
+ backgroundColor: "rgba(0,0,0,0.82)",
430
+ alignItems: "center",
431
+ justifyContent: "center",
432
+ },
433
+ pageSliderBubbleText: {
434
+ color: "#fff",
435
+ fontSize: 12,
436
+ fontWeight: "600",
437
+ },
438
+ });
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { View, StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface ZoomablePageProps {
5
+ children: React.ReactNode;
6
+ width: number;
7
+ height: number;
8
+ style?: StyleProp<ViewStyle>;
9
+ }
10
+
11
+ export const ZoomablePage: React.FC<ZoomablePageProps> = ({
12
+ children,
13
+ width,
14
+ height,
15
+ style,
16
+ }) => {
17
+ return (
18
+ <View
19
+ style={[
20
+ {
21
+ width,
22
+ height,
23
+ overflow: 'hidden',
24
+ },
25
+ style,
26
+ ]}
27
+ >
28
+ {children}
29
+ </View>
30
+ );
31
+ };
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ import { PdfPage, PdfPageProps } from './PdfPage';
4
+ import { ZoomablePage } from './ZoomablePage';
5
+ import { useZoomableList } from './ZoomableList';
6
+
7
+ export interface ZoomablePdfPageProps extends PdfPageProps {
8
+ width?: number;
9
+ height: number;
10
+ style?: StyleProp<ViewStyle>;
11
+ }
12
+
13
+ export const ZoomablePdfPage: React.FC<ZoomablePdfPageProps> = (props) => {
14
+ const { width: propWidth, height, style, selectionEnabled, ...pdfProps } = props;
15
+ const { isPanning, isPinching, width: contextWidth } = useZoomableList();
16
+
17
+ const width = propWidth ?? contextWidth;
18
+ const isInteracting = isPanning || isPinching;
19
+ const shouldEnableSelection = selectionEnabled !== undefined
20
+ ? (selectionEnabled && !isInteracting)
21
+ : !isInteracting;
22
+
23
+ return (
24
+ <ZoomablePage width={width} height={height} style={style}>
25
+ <PdfPage
26
+ width={width}
27
+ height={height}
28
+ style={{ width, height }}
29
+ selectionEnabled={shouldEnableSelection}
30
+ {...pdfProps}
31
+ />
32
+ </ZoomablePage>
33
+ );
34
+ };
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Reexport the native module. On web, it will be resolved to RnPdfKingModule.web.ts
2
+ // and on native platforms to RnPdfKingModule.ts
3
+ export { default } from './RnPdfKingModule';
4
+ export * from './RnPdfKing.types';
5
+ export * from './PdfDocument';
6
+ export * from './PdfPage';
7
+ export * from './ZoomableList';
8
+ export * from './ZoomablePage';
9
+ export * from './ZoomablePdfPage';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Default zoom scale constraints
3
+ * MAX_SCALE: maximum allowed zoom level
4
+ */
5
+ export const MAX_SCALE = 4
6
+
7
+ /**
8
+ * Double-tap zoom scale (Apple Photos uses 2x)
9
+ */
10
+ export const DOUBLE_TAP_SCALE = 2
11
+
12
+ /**
13
+ * Animation configuration constants
14
+ */
15
+ export const ANIMATION_DURATION = 350
16
+
17
+ /**
18
+ * Gesture detection thresholds
19
+ */
20
+ export const TAP_MAX_DELTA = 25
21
+ export const PAN_DEBOUNCE_MS = 10
22
+
23
+ /**
24
+ * Minimum number of pointers for pan gesture
25
+ */
26
+ export const MIN_PAN_POINTERS = 2
27
+ export const MAX_PAN_POINTERS = 2
28
+
29
+ /**
30
+ * Grouped zoom configuration
31
+ */
32
+ export const ZOOM_CONFIG = {
33
+ MAX_SCALE,
34
+ DOUBLE_TAP_SCALE,
35
+ ANIMATION_DURATION,
36
+ TAP_MAX_DELTA,
37
+ PAN_DEBOUNCE_MS,
38
+ MIN_PAN_POINTERS,
39
+ MAX_PAN_POINTERS,
40
+ } as const