dpv007 commited on
Commit
2c5bbee
·
1 Parent(s): 979d2d1

Use modern Gesture API v2 + reanimated for pinch zoom and scrubber

Browse files
keystone-app/components/CustomVideoPlayer.tsx CHANGED
@@ -1,14 +1,15 @@
1
  import React, { useEffect, useState, useRef } from 'react';
2
- import {
3
- View,
4
- StyleSheet,
5
- TouchableOpacity,
6
- Text,
7
  TouchableWithoutFeedback,
8
  } from 'react-native';
9
  import { useVideoPlayer, VideoView } from 'expo-video';
10
  import { MaterialIcons } from '@expo/vector-icons';
11
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
12
 
13
  interface CustomVideoPlayerProps {
14
  uri: string;
@@ -17,8 +18,8 @@ interface CustomVideoPlayerProps {
17
  onScrubEnd?: () => void;
18
  }
19
 
20
- export default function CustomVideoPlayer({
21
- uri, shouldPlay, onScrubStart, onScrubEnd
22
  }: CustomVideoPlayerProps) {
23
  const insets = useSafeAreaInsets();
24
  const [isPlaying, setIsPlaying] = useState(shouldPlay);
@@ -27,7 +28,6 @@ export default function CustomVideoPlayer({
27
  const [showControls, setShowControls] = useState(true);
28
  const [scrubberWidth, setScrubberWidth] = useState(0);
29
 
30
- const scrubberViewRef = useRef<View>(null);
31
  const scrubberPageXRef = useRef(0);
32
  const controlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
33
 
@@ -36,7 +36,6 @@ export default function CustomVideoPlayer({
36
  if (shouldPlay) p.play();
37
  });
38
 
39
- // Sync external shouldPlay
40
  useEffect(() => {
41
  if (shouldPlay) {
42
  player.play();
@@ -47,7 +46,6 @@ export default function CustomVideoPlayer({
47
  }
48
  }, [shouldPlay, player]);
49
 
50
- // Poll for time updates
51
  useEffect(() => {
52
  const interval = setInterval(() => {
53
  if (player) {
@@ -58,7 +56,6 @@ export default function CustomVideoPlayer({
58
  return () => clearInterval(interval);
59
  }, [player]);
60
 
61
- // Auto-hide controls
62
  const resetControlsTimeout = () => {
63
  setShowControls(true);
64
  if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current);
@@ -98,17 +95,15 @@ export default function CustomVideoPlayer({
98
  }
99
  };
100
 
101
- // Seek using absolute pageX — reliable across nested views
102
- const seekFromPageX = (pageX: number) => {
103
  if (duration <= 0 || scrubberWidth <= 0) return;
104
- const relX = pageX - scrubberPageXRef.current;
105
  const pct = Math.max(0, Math.min(1, relX / scrubberWidth));
106
  const seekTime = pct * duration;
107
  player.currentTime = seekTime;
108
  setCurrentTime(seekTime);
109
  };
110
 
111
- // Measure scrubber's absolute screen position
112
  const handleScrubberLayout = () => {
113
  scrubberViewRef.current?.measureInWindow((x, _y, w, _h) => {
114
  scrubberPageXRef.current = x;
@@ -116,6 +111,29 @@ export default function CustomVideoPlayer({
116
  });
117
  };
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  const formatTime = (secs: number) => {
120
  const m = Math.floor(secs / 60);
121
  const s = Math.floor(secs % 60);
@@ -127,7 +145,7 @@ export default function CustomVideoPlayer({
127
  return (
128
  <TouchableWithoutFeedback onPress={handleScreenTap}>
129
  <View style={styles.container}>
130
- <VideoView
131
  style={styles.video}
132
  player={player}
133
  allowsFullscreen={false}
@@ -135,62 +153,37 @@ export default function CustomVideoPlayer({
135
  nativeControls={false}
136
  />
137
 
138
- {/* Custom Controls Overlay */}
139
  {showControls && (
140
  <View style={styles.overlay} pointerEvents="box-none">
141
-
142
- {/* Center Play/Pause */}
143
- <TouchableOpacity
144
- style={styles.playPauseBtn}
145
  onPress={togglePlayPause}
146
  activeOpacity={0.7}
147
  >
148
- <MaterialIcons
149
- name={isPlaying ? "pause" : "play-arrow"}
150
- size={48}
151
- color="#fff"
152
  />
153
  </TouchableOpacity>
154
 
155
- {/* Bottom Scrubber Bar */}
156
  <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 120 }]}>
157
  <Text style={styles.timeText}>{formatTime(currentTime)}</Text>
158
-
159
- {/*
160
- Scrubber uses the Responder System with Capture phase
161
- to intercept ALL touches before FlatList can steal them.
162
- onResponderTerminationRequest={false} prevents FlatList
163
- from reclaiming the gesture mid-drag.
164
- */}
165
- <View
166
- ref={scrubberViewRef}
167
- style={styles.scrubberContainer}
168
- onLayout={handleScrubberLayout}
169
- onStartShouldSetResponder={() => true}
170
- onStartShouldSetResponderCapture={() => true}
171
- onMoveShouldSetResponder={() => true}
172
- onMoveShouldSetResponderCapture={() => true}
173
- onResponderTerminationRequest={() => false}
174
- onResponderGrant={(e) => {
175
- onScrubStart?.();
176
- seekFromPageX(e.nativeEvent.pageX);
177
- }}
178
- onResponderMove={(e) => {
179
- seekFromPageX(e.nativeEvent.pageX);
180
- }}
181
- onResponderRelease={() => {
182
- onScrubEnd?.();
183
- resetControlsTimeout();
184
- }}
185
- onResponderTerminate={() => {
186
- onScrubEnd?.();
187
- }}
188
- >
189
- <View style={styles.scrubberTrack}>
190
- <View style={[styles.scrubberFill, { width: `${progressPercent}%` }]} />
191
- <View style={[styles.scrubberThumb, { left: `${progressPercent}%` }]} />
192
  </View>
193
- </View>
194
 
195
  <Text style={styles.timeText}>{formatTime(duration)}</Text>
196
  </View>
 
1
  import React, { useEffect, useState, useRef } from 'react';
2
+ import {
3
+ View,
4
+ StyleSheet,
5
+ TouchableOpacity,
6
+ Text,
7
  TouchableWithoutFeedback,
8
  } from 'react-native';
9
  import { useVideoPlayer, VideoView } from 'expo-video';
10
  import { MaterialIcons } from '@expo/vector-icons';
11
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
12
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
13
 
14
  interface CustomVideoPlayerProps {
15
  uri: string;
 
18
  onScrubEnd?: () => void;
19
  }
20
 
21
+ export default function CustomVideoPlayer({
22
+ uri, shouldPlay, onScrubStart, onScrubEnd,
23
  }: CustomVideoPlayerProps) {
24
  const insets = useSafeAreaInsets();
25
  const [isPlaying, setIsPlaying] = useState(shouldPlay);
 
28
  const [showControls, setShowControls] = useState(true);
29
  const [scrubberWidth, setScrubberWidth] = useState(0);
30
 
 
31
  const scrubberPageXRef = useRef(0);
32
  const controlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
33
 
 
36
  if (shouldPlay) p.play();
37
  });
38
 
 
39
  useEffect(() => {
40
  if (shouldPlay) {
41
  player.play();
 
46
  }
47
  }, [shouldPlay, player]);
48
 
 
49
  useEffect(() => {
50
  const interval = setInterval(() => {
51
  if (player) {
 
56
  return () => clearInterval(interval);
57
  }, [player]);
58
 
 
59
  const resetControlsTimeout = () => {
60
  setShowControls(true);
61
  if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current);
 
95
  }
96
  };
97
 
98
+ const seekFromAbsoluteX = (absX: number) => {
 
99
  if (duration <= 0 || scrubberWidth <= 0) return;
100
+ const relX = absX - scrubberPageXRef.current;
101
  const pct = Math.max(0, Math.min(1, relX / scrubberWidth));
102
  const seekTime = pct * duration;
103
  player.currentTime = seekTime;
104
  setCurrentTime(seekTime);
105
  };
106
 
 
107
  const handleScrubberLayout = () => {
108
  scrubberViewRef.current?.measureInWindow((x, _y, w, _h) => {
109
  scrubberPageXRef.current = x;
 
111
  });
112
  };
113
 
114
+ const scrubberViewRef = useRef<View>(null);
115
+
116
+ /* ── Scrubber gesture ──
117
+ .runOnJS(true) → callbacks run on JS thread (no worklet needed)
118
+ .minDistance(0) → activates immediately on touch (tap-to-seek works)
119
+ This gesture is inside the FlatList item, and RNGH's FlatList
120
+ properly yields to nested gesture handlers.
121
+ */
122
+ const scrubGesture = Gesture.Pan()
123
+ .runOnJS(true)
124
+ .minDistance(0)
125
+ .onStart((e) => {
126
+ onScrubStart?.();
127
+ seekFromAbsoluteX(e.absoluteX);
128
+ })
129
+ .onUpdate((e) => {
130
+ seekFromAbsoluteX(e.absoluteX);
131
+ })
132
+ .onEnd(() => {
133
+ onScrubEnd?.();
134
+ resetControlsTimeout();
135
+ });
136
+
137
  const formatTime = (secs: number) => {
138
  const m = Math.floor(secs / 60);
139
  const s = Math.floor(secs % 60);
 
145
  return (
146
  <TouchableWithoutFeedback onPress={handleScreenTap}>
147
  <View style={styles.container}>
148
+ <VideoView
149
  style={styles.video}
150
  player={player}
151
  allowsFullscreen={false}
 
153
  nativeControls={false}
154
  />
155
 
 
156
  {showControls && (
157
  <View style={styles.overlay} pointerEvents="box-none">
158
+ <TouchableOpacity
159
+ style={styles.playPauseBtn}
 
 
160
  onPress={togglePlayPause}
161
  activeOpacity={0.7}
162
  >
163
+ <MaterialIcons
164
+ name={isPlaying ? 'pause' : 'play-arrow'}
165
+ size={48}
166
+ color="#fff"
167
  />
168
  </TouchableOpacity>
169
 
 
170
  <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 120 }]}>
171
  <Text style={styles.timeText}>{formatTime(currentTime)}</Text>
172
+
173
+ {/* Scrubber — wrapped in GestureDetector so RNGH handles
174
+ touch negotiation with the parent FlatList */}
175
+ <GestureDetector gesture={scrubGesture}>
176
+ <View
177
+ ref={scrubberViewRef}
178
+ style={styles.scrubberContainer}
179
+ onLayout={handleScrubberLayout}
180
+ >
181
+ <View style={styles.scrubberTrack}>
182
+ <View style={[styles.scrubberFill, { width: `${progressPercent}%` }]} />
183
+ <View style={[styles.scrubberThumb, { left: `${progressPercent}%` }]} />
184
+ </View>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  </View>
186
+ </GestureDetector>
187
 
188
  <Text style={styles.timeText}>{formatTime(duration)}</Text>
189
  </View>
keystone-app/components/FullScreenSwiper.tsx CHANGED
@@ -1,14 +1,14 @@
1
  import React, { useRef, useState, useEffect } from 'react';
2
- import {
3
- View,
4
- StyleSheet,
5
- FlatList,
6
- Platform,
7
- Animated,
8
- useWindowDimensions,
9
- } from 'react-native';
10
  import { Image } from 'expo-image';
11
- import { PinchGestureHandler, PanGestureHandler, State } from 'react-native-gesture-handler';
 
 
 
 
 
 
 
12
  import CustomVideoPlayer from './CustomVideoPlayer';
13
 
14
  interface FullScreenSwiperProps {
@@ -20,138 +20,102 @@ interface FullScreenSwiperProps {
20
 
21
  /* ─────────────────────────────────────────────────────────────
22
  ZoomableImage
23
- • Pinch-to-zoom via PinchGestureHandler (native multi-touch)
24
- • Swipe-down-to-close via PanGestureHandler (vertical only)
25
- • Both run on the native thread for 60 fps
26
  ───────────────────────────────────────────────────────────── */
27
- function ZoomableImage({
28
- uri, thumbUri, width, height, onSwipeDown, onZoomChange
29
- }: {
30
- uri: string; thumbUri?: string; width: number; height: number;
31
  onSwipeDown: () => void; onZoomChange: (zooming: boolean) => void;
32
  }) {
33
- /* ── Pinch zoom state ── */
34
- const baseScaleAnim = useRef(new Animated.Value(1)).current;
35
- const pinchScaleAnim = useRef(new Animated.Value(1)).current;
36
- const baseScaleRef = useRef(1);
37
- // displayScale = baseScale × pinchScale — computed on native thread
38
- const displayScale = Animated.multiply(baseScaleAnim, pinchScaleAnim);
39
-
40
- /* ── Swipe-down state ── */
41
- const translateY = useRef(new Animated.Value(0)).current;
42
-
43
- /* ── Handler refs for simultaneousHandlers ── */
44
- const pinchRef = useRef(null);
45
- const panRef = useRef(null);
46
-
47
- /* ── Callback refs (prevent stale closures) ── */
48
- const onSwipeDownRef = useRef(onSwipeDown);
49
- const onZoomChangeRef = useRef(onZoomChange);
50
- useEffect(() => { onSwipeDownRef.current = onSwipeDown; }, [onSwipeDown]);
51
- useEffect(() => { onZoomChangeRef.current = onZoomChange; }, [onZoomChange]);
52
-
53
- /* ── Pinch handlers ── */
54
- const onPinchEvent = Animated.event(
55
- [{ nativeEvent: { scale: pinchScaleAnim } }],
56
- { useNativeDriver: true }
57
- );
58
-
59
- const onPinchStateChange = (e: any) => {
60
- if (e.nativeEvent.state === State.ACTIVE) {
61
- onZoomChangeRef.current(true); // disable FlatList scroll
62
- }
63
- if (e.nativeEvent.oldState === State.ACTIVE) {
64
- onZoomChangeRef.current(false); // re-enable FlatList scroll
65
- const newBase = baseScaleRef.current * e.nativeEvent.scale;
66
- if (newBase < 1.15) {
67
- // Snap back to 1×
68
- baseScaleRef.current = 1;
69
- baseScaleAnim.setValue(1);
70
- Animated.spring(pinchScaleAnim, {
71
- toValue: 1, useNativeDriver: true, bounciness: 0,
72
- }).start();
73
  } else {
74
- // Keep the zoom level
75
- const clamped = Math.min(5, newBase);
76
- baseScaleRef.current = clamped;
77
- baseScaleAnim.setValue(clamped);
78
- pinchScaleAnim.setValue(1);
79
  }
80
- }
81
- };
82
-
83
- /* ── Pan (swipe-down) handlers ── */
84
- const onPanEvent = Animated.event(
85
- [{ nativeEvent: { translationY: translateY } }],
86
- { useNativeDriver: true }
87
- );
88
-
89
- const onPanStateChange = (e: any) => {
90
- if (e.nativeEvent.state === State.ACTIVE) {
91
- onZoomChangeRef.current(true);
92
- }
93
- if (e.nativeEvent.oldState === State.ACTIVE) {
94
- onZoomChangeRef.current(false);
95
- if (e.nativeEvent.translationY > 100) {
96
- onSwipeDownRef.current();
 
 
 
97
  } else {
98
- Animated.spring(translateY, {
99
- toValue: 0, useNativeDriver: true, bounciness: 0,
100
- }).start();
101
  }
102
- }
103
- };
 
 
 
 
 
 
 
 
104
 
105
  return (
106
- <PanGestureHandler
107
- ref={panRef}
108
- simultaneousHandlers={pinchRef}
109
- minPointers={1}
110
- maxPointers={1}
111
- activeOffsetY={20}
112
- failOffsetX={[-15, 15]}
113
- onGestureEvent={onPanEvent}
114
- onHandlerStateChange={onPanStateChange}
115
- >
116
- <Animated.View style={{ flex: 1 }}>
117
- <PinchGestureHandler
118
- ref={pinchRef}
119
- simultaneousHandlers={panRef}
120
- onGestureEvent={onPinchEvent}
121
- onHandlerStateChange={onPinchStateChange}
122
- >
123
- <Animated.View
124
- style={{
125
- width,
126
- height,
127
- justifyContent: 'center',
128
- alignItems: 'center',
129
- transform: [{ scale: displayScale }, { translateY }],
130
- }}
131
- >
132
- <Image
133
- source={{ uri }}
134
- placeholder={thumbUri ? { uri: thumbUri } : undefined}
135
- style={{ width: '100%', height: '100%' }}
136
- contentFit="contain"
137
- cachePolicy="memory-disk"
138
- transition={200}
139
- backgroundColor="transparent"
140
- allowDownscaling={false}
141
- />
142
- </Animated.View>
143
- </PinchGestureHandler>
144
  </Animated.View>
145
- </PanGestureHandler>
146
  );
147
  }
148
 
149
-
150
  /* ─────────────────────────────────────────────────────────────
151
  FullScreenSwiper
 
 
152
  ───────────────────────────────────────────────────────────── */
153
- export default function FullScreenSwiper({
154
- assets, initialIndex, onClose, onIndexChange
155
  }: FullScreenSwiperProps) {
156
  const { width, height } = useWindowDimensions();
157
  const [activeIndex, setActiveIndex] = useState(initialIndex);
 
1
  import React, { useRef, useState, useEffect } from 'react';
2
+ import { View, StyleSheet, Platform, useWindowDimensions } from 'react-native';
 
 
 
 
 
 
 
3
  import { Image } from 'expo-image';
4
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
5
+ import { FlatList } from 'react-native-gesture-handler';
6
+ import Animated, {
7
+ useSharedValue,
8
+ useAnimatedStyle,
9
+ withSpring,
10
+ runOnJS,
11
+ } from 'react-native-reanimated';
12
  import CustomVideoPlayer from './CustomVideoPlayer';
13
 
14
  interface FullScreenSwiperProps {
 
20
 
21
  /* ─────────────────────────────────────────────────────────────
22
  ZoomableImage
23
+ • Gesture.Pinch() — native multi-touch pinch-to-zoom
24
+ • Gesture.Pan() — single-finger swipe-down-to-close
25
+ • Both run on the UI thread via reanimated worklets
26
  ───────────────────────────────────────────────────────────── */
27
+ function ZoomableImage({
28
+ uri, thumbUri, width, height, onSwipeDown, onZoomChange,
29
+ }: {
30
+ uri: string; thumbUri?: string; width: number; height: number;
31
  onSwipeDown: () => void; onZoomChange: (zooming: boolean) => void;
32
  }) {
33
+ const scale = useSharedValue(1);
34
+ const savedScale = useSharedValue(1);
35
+ const translateY = useSharedValue(0);
36
+
37
+ const pinchGesture = Gesture.Pinch()
38
+ .onStart(() => {
39
+ 'worklet';
40
+ runOnJS(onZoomChange)(true);
41
+ })
42
+ .onUpdate((e) => {
43
+ 'worklet';
44
+ scale.value = Math.max(0.5, Math.min(5, savedScale.value * e.scale));
45
+ })
46
+ .onEnd(() => {
47
+ 'worklet';
48
+ runOnJS(onZoomChange)(false);
49
+ if (scale.value < 1.15) {
50
+ scale.value = withSpring(1, { damping: 15, stiffness: 150 });
51
+ savedScale.value = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  } else {
53
+ savedScale.value = Math.min(5, scale.value);
 
 
 
 
54
  }
55
+ });
56
+
57
+ const panGesture = Gesture.Pan()
58
+ .minPointers(1)
59
+ .maxPointers(1)
60
+ .activeOffsetY(20)
61
+ .failOffsetX([-15, 15])
62
+ .onStart(() => {
63
+ 'worklet';
64
+ runOnJS(onZoomChange)(true);
65
+ })
66
+ .onUpdate((e) => {
67
+ 'worklet';
68
+ translateY.value = e.translationY;
69
+ })
70
+ .onEnd((e) => {
71
+ 'worklet';
72
+ runOnJS(onZoomChange)(false);
73
+ if (e.translationY > 100) {
74
+ runOnJS(onSwipeDown)();
75
  } else {
76
+ translateY.value = withSpring(0, { damping: 15, stiffness: 150 });
 
 
77
  }
78
+ });
79
+
80
+ const composedGesture = Gesture.Simultaneous(pinchGesture, panGesture);
81
+
82
+ const animatedStyle = useAnimatedStyle(() => ({
83
+ transform: [
84
+ { scale: scale.value },
85
+ { translateY: translateY.value },
86
+ ],
87
+ }));
88
 
89
  return (
90
+ <GestureDetector gesture={composedGesture}>
91
+ <Animated.View
92
+ style={[
93
+ { width, height, justifyContent: 'center', alignItems: 'center' },
94
+ animatedStyle,
95
+ ]}
96
+ >
97
+ <Image
98
+ source={{ uri }}
99
+ placeholder={thumbUri ? { uri: thumbUri } : undefined}
100
+ style={{ width: '100%', height: '100%' }}
101
+ contentFit="contain"
102
+ cachePolicy="memory-disk"
103
+ transition={200}
104
+ backgroundColor="transparent"
105
+ allowDownscaling={false}
106
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  </Animated.View>
108
+ </GestureDetector>
109
  );
110
  }
111
 
 
112
  /* ─────────────────────────────────────────────────────────────
113
  FullScreenSwiper
114
+ Uses RNGH's FlatList so gesture handlers inside items
115
+ properly negotiate with the scroll gesture on Android.
116
  ───────────────────────────────────────────────────────────── */
117
+ export default function FullScreenSwiper({
118
+ assets, initialIndex, onClose, onIndexChange,
119
  }: FullScreenSwiperProps) {
120
  const { width, height } = useWindowDimensions();
121
  const [activeIndex, setActiveIndex] = useState(initialIndex);
keystone-app/package-lock.json CHANGED
@@ -31,6 +31,7 @@
31
  "react-dom": "19.1.0",
32
  "react-native": "0.81.5",
33
  "react-native-gesture-handler": "~2.28.0",
 
34
  "react-native-safe-area-context": "~5.6.0",
35
  "react-native-screens": "~4.16.0",
36
  "react-native-web": "^0.21.2"
@@ -1171,6 +1172,22 @@
1171
  "@babel/core": "^7.0.0-0"
1172
  }
1173
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1174
  "node_modules/@babel/plugin-transform-typescript": {
1175
  "version": "7.29.7",
1176
  "license": "MIT",
@@ -2999,6 +3016,556 @@
2999
  "node": ">= 20.19.4"
3000
  }
3001
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3002
  "node_modules/@react-native/normalize-colors": {
3003
  "version": "0.81.5",
3004
  "license": "MIT"
@@ -7888,6 +8455,21 @@
7888
  "react-native": "*"
7889
  }
7890
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7891
  "node_modules/react-native-safe-area-context": {
7892
  "version": "5.6.2",
7893
  "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
@@ -7939,6 +8521,32 @@
7939
  "version": "6.0.0",
7940
  "license": "MIT"
7941
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7942
  "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
7943
  "version": "0.81.5",
7944
  "license": "MIT",
 
31
  "react-dom": "19.1.0",
32
  "react-native": "0.81.5",
33
  "react-native-gesture-handler": "~2.28.0",
34
+ "react-native-reanimated": "~4.1.1",
35
  "react-native-safe-area-context": "~5.6.0",
36
  "react-native-screens": "~4.16.0",
37
  "react-native-web": "^0.21.2"
 
1172
  "@babel/core": "^7.0.0-0"
1173
  }
1174
  },
1175
+ "node_modules/@babel/plugin-transform-template-literals": {
1176
+ "version": "7.29.7",
1177
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
1178
+ "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
1179
+ "license": "MIT",
1180
+ "peer": true,
1181
+ "dependencies": {
1182
+ "@babel/helper-plugin-utils": "^7.29.7"
1183
+ },
1184
+ "engines": {
1185
+ "node": ">=6.9.0"
1186
+ },
1187
+ "peerDependencies": {
1188
+ "@babel/core": "^7.0.0-0"
1189
+ }
1190
+ },
1191
  "node_modules/@babel/plugin-transform-typescript": {
1192
  "version": "7.29.7",
1193
  "license": "MIT",
 
3016
  "node": ">= 20.19.4"
3017
  }
3018
  },
3019
+ "node_modules/@react-native/metro-babel-transformer": {
3020
+ "version": "0.86.0",
3021
+ "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.0.tgz",
3022
+ "integrity": "sha512-SjKej3E5qIahqo/G+rSOrmJUQM44RyKtWtO+VfmKAAMoJWkBFomM22hTLKCIS5cdbIAJ9COAmU+KAi2wVSO0wQ==",
3023
+ "license": "MIT",
3024
+ "peer": true,
3025
+ "dependencies": {
3026
+ "@babel/core": "^7.25.2",
3027
+ "@react-native/babel-preset": "0.86.0",
3028
+ "hermes-parser": "0.36.0",
3029
+ "nullthrows": "^1.1.1"
3030
+ },
3031
+ "engines": {
3032
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3033
+ },
3034
+ "peerDependencies": {
3035
+ "@babel/core": "*"
3036
+ }
3037
+ },
3038
+ "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": {
3039
+ "version": "0.86.0",
3040
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.0.tgz",
3041
+ "integrity": "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==",
3042
+ "license": "MIT",
3043
+ "peer": true,
3044
+ "dependencies": {
3045
+ "@babel/traverse": "^7.29.0",
3046
+ "@react-native/codegen": "0.86.0"
3047
+ },
3048
+ "engines": {
3049
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3050
+ }
3051
+ },
3052
+ "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": {
3053
+ "version": "0.86.0",
3054
+ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.0.tgz",
3055
+ "integrity": "sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==",
3056
+ "license": "MIT",
3057
+ "peer": true,
3058
+ "dependencies": {
3059
+ "@babel/core": "^7.25.2",
3060
+ "@babel/plugin-proposal-export-default-from": "^7.24.7",
3061
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
3062
+ "@babel/plugin-syntax-export-default-from": "^7.24.7",
3063
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
3064
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
3065
+ "@babel/plugin-transform-async-generator-functions": "^7.25.4",
3066
+ "@babel/plugin-transform-async-to-generator": "^7.24.7",
3067
+ "@babel/plugin-transform-block-scoping": "^7.25.0",
3068
+ "@babel/plugin-transform-class-properties": "^7.25.4",
3069
+ "@babel/plugin-transform-classes": "^7.25.4",
3070
+ "@babel/plugin-transform-destructuring": "^7.24.8",
3071
+ "@babel/plugin-transform-flow-strip-types": "^7.25.2",
3072
+ "@babel/plugin-transform-for-of": "^7.24.7",
3073
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
3074
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
3075
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
3076
+ "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
3077
+ "@babel/plugin-transform-optional-chaining": "^7.24.8",
3078
+ "@babel/plugin-transform-private-methods": "^7.24.7",
3079
+ "@babel/plugin-transform-private-property-in-object": "^7.24.7",
3080
+ "@babel/plugin-transform-react-display-name": "^7.24.7",
3081
+ "@babel/plugin-transform-react-jsx": "^7.25.2",
3082
+ "@babel/plugin-transform-react-jsx-self": "^7.24.7",
3083
+ "@babel/plugin-transform-react-jsx-source": "^7.24.7",
3084
+ "@babel/plugin-transform-regenerator": "^7.24.7",
3085
+ "@babel/plugin-transform-runtime": "^7.24.7",
3086
+ "@babel/plugin-transform-typescript": "^7.25.2",
3087
+ "@babel/plugin-transform-unicode-regex": "^7.24.7",
3088
+ "@react-native/babel-plugin-codegen": "0.86.0",
3089
+ "babel-plugin-syntax-hermes-parser": "0.36.0",
3090
+ "babel-plugin-transform-flow-enums": "^0.0.2",
3091
+ "react-refresh": "^0.14.0"
3092
+ },
3093
+ "engines": {
3094
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3095
+ },
3096
+ "peerDependencies": {
3097
+ "@babel/core": "*"
3098
+ }
3099
+ },
3100
+ "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/codegen": {
3101
+ "version": "0.86.0",
3102
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz",
3103
+ "integrity": "sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==",
3104
+ "license": "MIT",
3105
+ "peer": true,
3106
+ "dependencies": {
3107
+ "@babel/core": "^7.25.2",
3108
+ "@babel/parser": "^7.29.0",
3109
+ "hermes-parser": "0.36.0",
3110
+ "invariant": "^2.2.4",
3111
+ "nullthrows": "^1.1.1",
3112
+ "tinyglobby": "^0.2.15",
3113
+ "yargs": "^17.6.2"
3114
+ },
3115
+ "engines": {
3116
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3117
+ },
3118
+ "peerDependencies": {
3119
+ "@babel/core": "*"
3120
+ }
3121
+ },
3122
+ "node_modules/@react-native/metro-babel-transformer/node_modules/babel-plugin-syntax-hermes-parser": {
3123
+ "version": "0.36.0",
3124
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
3125
+ "integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
3126
+ "license": "MIT",
3127
+ "peer": true,
3128
+ "dependencies": {
3129
+ "hermes-parser": "0.36.0"
3130
+ }
3131
+ },
3132
+ "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": {
3133
+ "version": "0.36.0",
3134
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
3135
+ "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
3136
+ "license": "MIT",
3137
+ "peer": true
3138
+ },
3139
+ "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": {
3140
+ "version": "0.36.0",
3141
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
3142
+ "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
3143
+ "license": "MIT",
3144
+ "peer": true,
3145
+ "dependencies": {
3146
+ "hermes-estree": "0.36.0"
3147
+ }
3148
+ },
3149
+ "node_modules/@react-native/metro-config": {
3150
+ "version": "0.86.0",
3151
+ "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.0.tgz",
3152
+ "integrity": "sha512-7v+xbTeEci9ZcQ/Z1OqI4RXcqN69wSMDYL5BAMvOReZ7U04+aDQ0/SQhClYPn6x2/RxM4WzMKSAuNyLKqvYVtw==",
3153
+ "license": "MIT",
3154
+ "peer": true,
3155
+ "dependencies": {
3156
+ "@react-native/js-polyfills": "0.86.0",
3157
+ "@react-native/metro-babel-transformer": "0.86.0",
3158
+ "metro-config": "^0.84.3",
3159
+ "metro-runtime": "^0.84.3"
3160
+ },
3161
+ "engines": {
3162
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3163
+ }
3164
+ },
3165
+ "node_modules/@react-native/metro-config/node_modules/@babel/code-frame": {
3166
+ "version": "7.29.7",
3167
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
3168
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
3169
+ "license": "MIT",
3170
+ "peer": true,
3171
+ "dependencies": {
3172
+ "@babel/helper-validator-identifier": "^7.29.7",
3173
+ "js-tokens": "^4.0.0",
3174
+ "picocolors": "^1.1.1"
3175
+ },
3176
+ "engines": {
3177
+ "node": ">=6.9.0"
3178
+ }
3179
+ },
3180
+ "node_modules/@react-native/metro-config/node_modules/@react-native/js-polyfills": {
3181
+ "version": "0.86.0",
3182
+ "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.0.tgz",
3183
+ "integrity": "sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==",
3184
+ "license": "MIT",
3185
+ "peer": true,
3186
+ "engines": {
3187
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3188
+ }
3189
+ },
3190
+ "node_modules/@react-native/metro-config/node_modules/accepts": {
3191
+ "version": "2.0.0",
3192
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
3193
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
3194
+ "license": "MIT",
3195
+ "peer": true,
3196
+ "dependencies": {
3197
+ "mime-types": "^3.0.0",
3198
+ "negotiator": "^1.0.0"
3199
+ },
3200
+ "engines": {
3201
+ "node": ">= 0.6"
3202
+ }
3203
+ },
3204
+ "node_modules/@react-native/metro-config/node_modules/hermes-estree": {
3205
+ "version": "0.35.0",
3206
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
3207
+ "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
3208
+ "license": "MIT",
3209
+ "peer": true
3210
+ },
3211
+ "node_modules/@react-native/metro-config/node_modules/hermes-parser": {
3212
+ "version": "0.35.0",
3213
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
3214
+ "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
3215
+ "license": "MIT",
3216
+ "peer": true,
3217
+ "dependencies": {
3218
+ "hermes-estree": "0.35.0"
3219
+ }
3220
+ },
3221
+ "node_modules/@react-native/metro-config/node_modules/metro": {
3222
+ "version": "0.84.4",
3223
+ "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.4.tgz",
3224
+ "integrity": "sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==",
3225
+ "license": "MIT",
3226
+ "peer": true,
3227
+ "dependencies": {
3228
+ "@babel/code-frame": "^7.29.0",
3229
+ "@babel/core": "^7.25.2",
3230
+ "@babel/generator": "^7.29.1",
3231
+ "@babel/parser": "^7.29.0",
3232
+ "@babel/template": "^7.28.6",
3233
+ "@babel/traverse": "^7.29.0",
3234
+ "@babel/types": "^7.29.0",
3235
+ "accepts": "^2.0.0",
3236
+ "ci-info": "^2.0.0",
3237
+ "connect": "^3.6.5",
3238
+ "debug": "^4.4.0",
3239
+ "error-stack-parser": "^2.0.6",
3240
+ "flow-enums-runtime": "^0.0.6",
3241
+ "graceful-fs": "^4.2.4",
3242
+ "hermes-parser": "0.35.0",
3243
+ "image-size": "^1.0.2",
3244
+ "invariant": "^2.2.4",
3245
+ "jest-worker": "^29.7.0",
3246
+ "jsc-safe-url": "^0.2.2",
3247
+ "lodash.throttle": "^4.1.1",
3248
+ "metro-babel-transformer": "0.84.4",
3249
+ "metro-cache": "0.84.4",
3250
+ "metro-cache-key": "0.84.4",
3251
+ "metro-config": "0.84.4",
3252
+ "metro-core": "0.84.4",
3253
+ "metro-file-map": "0.84.4",
3254
+ "metro-resolver": "0.84.4",
3255
+ "metro-runtime": "0.84.4",
3256
+ "metro-source-map": "0.84.4",
3257
+ "metro-symbolicate": "0.84.4",
3258
+ "metro-transform-plugins": "0.84.4",
3259
+ "metro-transform-worker": "0.84.4",
3260
+ "mime-types": "^3.0.1",
3261
+ "nullthrows": "^1.1.1",
3262
+ "serialize-error": "^2.1.0",
3263
+ "source-map": "^0.5.6",
3264
+ "throat": "^5.0.0",
3265
+ "ws": "^7.5.10",
3266
+ "yargs": "^17.6.2"
3267
+ },
3268
+ "bin": {
3269
+ "metro": "src/cli.js"
3270
+ },
3271
+ "engines": {
3272
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3273
+ }
3274
+ },
3275
+ "node_modules/@react-native/metro-config/node_modules/metro-babel-transformer": {
3276
+ "version": "0.84.4",
3277
+ "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.4.tgz",
3278
+ "integrity": "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==",
3279
+ "license": "MIT",
3280
+ "peer": true,
3281
+ "dependencies": {
3282
+ "@babel/core": "^7.25.2",
3283
+ "flow-enums-runtime": "^0.0.6",
3284
+ "hermes-parser": "0.35.0",
3285
+ "metro-cache-key": "0.84.4",
3286
+ "nullthrows": "^1.1.1"
3287
+ },
3288
+ "engines": {
3289
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3290
+ }
3291
+ },
3292
+ "node_modules/@react-native/metro-config/node_modules/metro-cache": {
3293
+ "version": "0.84.4",
3294
+ "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.4.tgz",
3295
+ "integrity": "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==",
3296
+ "license": "MIT",
3297
+ "peer": true,
3298
+ "dependencies": {
3299
+ "exponential-backoff": "^3.1.1",
3300
+ "flow-enums-runtime": "^0.0.6",
3301
+ "https-proxy-agent": "^7.0.5",
3302
+ "metro-core": "0.84.4"
3303
+ },
3304
+ "engines": {
3305
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3306
+ }
3307
+ },
3308
+ "node_modules/@react-native/metro-config/node_modules/metro-cache-key": {
3309
+ "version": "0.84.4",
3310
+ "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.4.tgz",
3311
+ "integrity": "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==",
3312
+ "license": "MIT",
3313
+ "peer": true,
3314
+ "dependencies": {
3315
+ "flow-enums-runtime": "^0.0.6"
3316
+ },
3317
+ "engines": {
3318
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3319
+ }
3320
+ },
3321
+ "node_modules/@react-native/metro-config/node_modules/metro-config": {
3322
+ "version": "0.84.4",
3323
+ "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.4.tgz",
3324
+ "integrity": "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==",
3325
+ "license": "MIT",
3326
+ "peer": true,
3327
+ "dependencies": {
3328
+ "connect": "^3.6.5",
3329
+ "flow-enums-runtime": "^0.0.6",
3330
+ "jest-validate": "^29.7.0",
3331
+ "metro": "0.84.4",
3332
+ "metro-cache": "0.84.4",
3333
+ "metro-core": "0.84.4",
3334
+ "metro-runtime": "0.84.4",
3335
+ "yaml": "^2.6.1"
3336
+ },
3337
+ "engines": {
3338
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3339
+ }
3340
+ },
3341
+ "node_modules/@react-native/metro-config/node_modules/metro-core": {
3342
+ "version": "0.84.4",
3343
+ "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.4.tgz",
3344
+ "integrity": "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==",
3345
+ "license": "MIT",
3346
+ "peer": true,
3347
+ "dependencies": {
3348
+ "flow-enums-runtime": "^0.0.6",
3349
+ "lodash.throttle": "^4.1.1",
3350
+ "metro-resolver": "0.84.4"
3351
+ },
3352
+ "engines": {
3353
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3354
+ }
3355
+ },
3356
+ "node_modules/@react-native/metro-config/node_modules/metro-file-map": {
3357
+ "version": "0.84.4",
3358
+ "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.4.tgz",
3359
+ "integrity": "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==",
3360
+ "license": "MIT",
3361
+ "peer": true,
3362
+ "dependencies": {
3363
+ "debug": "^4.4.0",
3364
+ "fb-watchman": "^2.0.0",
3365
+ "flow-enums-runtime": "^0.0.6",
3366
+ "graceful-fs": "^4.2.4",
3367
+ "invariant": "^2.2.4",
3368
+ "jest-worker": "^29.7.0",
3369
+ "micromatch": "^4.0.4",
3370
+ "nullthrows": "^1.1.1",
3371
+ "walker": "^1.0.7"
3372
+ },
3373
+ "engines": {
3374
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3375
+ }
3376
+ },
3377
+ "node_modules/@react-native/metro-config/node_modules/metro-minify-terser": {
3378
+ "version": "0.84.4",
3379
+ "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.4.tgz",
3380
+ "integrity": "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==",
3381
+ "license": "MIT",
3382
+ "peer": true,
3383
+ "dependencies": {
3384
+ "flow-enums-runtime": "^0.0.6",
3385
+ "terser": "^5.15.0"
3386
+ },
3387
+ "engines": {
3388
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3389
+ }
3390
+ },
3391
+ "node_modules/@react-native/metro-config/node_modules/metro-resolver": {
3392
+ "version": "0.84.4",
3393
+ "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.4.tgz",
3394
+ "integrity": "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==",
3395
+ "license": "MIT",
3396
+ "peer": true,
3397
+ "dependencies": {
3398
+ "flow-enums-runtime": "^0.0.6"
3399
+ },
3400
+ "engines": {
3401
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3402
+ }
3403
+ },
3404
+ "node_modules/@react-native/metro-config/node_modules/metro-runtime": {
3405
+ "version": "0.84.4",
3406
+ "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.4.tgz",
3407
+ "integrity": "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==",
3408
+ "license": "MIT",
3409
+ "peer": true,
3410
+ "dependencies": {
3411
+ "@babel/runtime": "^7.25.0",
3412
+ "flow-enums-runtime": "^0.0.6"
3413
+ },
3414
+ "engines": {
3415
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3416
+ }
3417
+ },
3418
+ "node_modules/@react-native/metro-config/node_modules/metro-source-map": {
3419
+ "version": "0.84.4",
3420
+ "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.4.tgz",
3421
+ "integrity": "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==",
3422
+ "license": "MIT",
3423
+ "peer": true,
3424
+ "dependencies": {
3425
+ "@babel/traverse": "^7.29.0",
3426
+ "@babel/types": "^7.29.0",
3427
+ "flow-enums-runtime": "^0.0.6",
3428
+ "invariant": "^2.2.4",
3429
+ "metro-symbolicate": "0.84.4",
3430
+ "nullthrows": "^1.1.1",
3431
+ "ob1": "0.84.4",
3432
+ "source-map": "^0.5.6",
3433
+ "vlq": "^1.0.0"
3434
+ },
3435
+ "engines": {
3436
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3437
+ }
3438
+ },
3439
+ "node_modules/@react-native/metro-config/node_modules/metro-symbolicate": {
3440
+ "version": "0.84.4",
3441
+ "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.4.tgz",
3442
+ "integrity": "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==",
3443
+ "license": "MIT",
3444
+ "peer": true,
3445
+ "dependencies": {
3446
+ "flow-enums-runtime": "^0.0.6",
3447
+ "invariant": "^2.2.4",
3448
+ "metro-source-map": "0.84.4",
3449
+ "nullthrows": "^1.1.1",
3450
+ "source-map": "^0.5.6",
3451
+ "vlq": "^1.0.0"
3452
+ },
3453
+ "bin": {
3454
+ "metro-symbolicate": "src/index.js"
3455
+ },
3456
+ "engines": {
3457
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3458
+ }
3459
+ },
3460
+ "node_modules/@react-native/metro-config/node_modules/metro-transform-plugins": {
3461
+ "version": "0.84.4",
3462
+ "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.4.tgz",
3463
+ "integrity": "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==",
3464
+ "license": "MIT",
3465
+ "peer": true,
3466
+ "dependencies": {
3467
+ "@babel/core": "^7.25.2",
3468
+ "@babel/generator": "^7.29.1",
3469
+ "@babel/template": "^7.28.6",
3470
+ "@babel/traverse": "^7.29.0",
3471
+ "flow-enums-runtime": "^0.0.6",
3472
+ "nullthrows": "^1.1.1"
3473
+ },
3474
+ "engines": {
3475
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3476
+ }
3477
+ },
3478
+ "node_modules/@react-native/metro-config/node_modules/metro-transform-worker": {
3479
+ "version": "0.84.4",
3480
+ "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.4.tgz",
3481
+ "integrity": "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==",
3482
+ "license": "MIT",
3483
+ "peer": true,
3484
+ "dependencies": {
3485
+ "@babel/core": "^7.25.2",
3486
+ "@babel/generator": "^7.29.1",
3487
+ "@babel/parser": "^7.29.0",
3488
+ "@babel/types": "^7.29.0",
3489
+ "flow-enums-runtime": "^0.0.6",
3490
+ "metro": "0.84.4",
3491
+ "metro-babel-transformer": "0.84.4",
3492
+ "metro-cache": "0.84.4",
3493
+ "metro-cache-key": "0.84.4",
3494
+ "metro-minify-terser": "0.84.4",
3495
+ "metro-source-map": "0.84.4",
3496
+ "metro-transform-plugins": "0.84.4",
3497
+ "nullthrows": "^1.1.1"
3498
+ },
3499
+ "engines": {
3500
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3501
+ }
3502
+ },
3503
+ "node_modules/@react-native/metro-config/node_modules/mime-db": {
3504
+ "version": "1.54.0",
3505
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
3506
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
3507
+ "license": "MIT",
3508
+ "peer": true,
3509
+ "engines": {
3510
+ "node": ">= 0.6"
3511
+ }
3512
+ },
3513
+ "node_modules/@react-native/metro-config/node_modules/mime-types": {
3514
+ "version": "3.0.2",
3515
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
3516
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
3517
+ "license": "MIT",
3518
+ "peer": true,
3519
+ "dependencies": {
3520
+ "mime-db": "^1.54.0"
3521
+ },
3522
+ "engines": {
3523
+ "node": ">=18"
3524
+ },
3525
+ "funding": {
3526
+ "type": "opencollective",
3527
+ "url": "https://opencollective.com/express"
3528
+ }
3529
+ },
3530
+ "node_modules/@react-native/metro-config/node_modules/negotiator": {
3531
+ "version": "1.0.0",
3532
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
3533
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
3534
+ "license": "MIT",
3535
+ "peer": true,
3536
+ "engines": {
3537
+ "node": ">= 0.6"
3538
+ }
3539
+ },
3540
+ "node_modules/@react-native/metro-config/node_modules/ob1": {
3541
+ "version": "0.84.4",
3542
+ "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz",
3543
+ "integrity": "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==",
3544
+ "license": "MIT",
3545
+ "peer": true,
3546
+ "dependencies": {
3547
+ "flow-enums-runtime": "^0.0.6"
3548
+ },
3549
+ "engines": {
3550
+ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
3551
+ }
3552
+ },
3553
+ "node_modules/@react-native/metro-config/node_modules/yaml": {
3554
+ "version": "2.9.0",
3555
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
3556
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
3557
+ "license": "ISC",
3558
+ "peer": true,
3559
+ "bin": {
3560
+ "yaml": "bin.mjs"
3561
+ },
3562
+ "engines": {
3563
+ "node": ">= 14.6"
3564
+ },
3565
+ "funding": {
3566
+ "url": "https://github.com/sponsors/eemeli"
3567
+ }
3568
+ },
3569
  "node_modules/@react-native/normalize-colors": {
3570
  "version": "0.81.5",
3571
  "license": "MIT"
 
8455
  "react-native": "*"
8456
  }
8457
  },
8458
+ "node_modules/react-native-reanimated": {
8459
+ "version": "4.1.7",
8460
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz",
8461
+ "integrity": "sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==",
8462
+ "license": "MIT",
8463
+ "dependencies": {
8464
+ "react-native-is-edge-to-edge": "^1.2.1",
8465
+ "semver": "^7.7.2"
8466
+ },
8467
+ "peerDependencies": {
8468
+ "react": "*",
8469
+ "react-native": "0.78 - 0.82",
8470
+ "react-native-worklets": "0.5 - 0.8"
8471
+ }
8472
+ },
8473
  "node_modules/react-native-safe-area-context": {
8474
  "version": "5.6.2",
8475
  "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
 
8521
  "version": "6.0.0",
8522
  "license": "MIT"
8523
  },
8524
+ "node_modules/react-native-worklets": {
8525
+ "version": "0.8.3",
8526
+ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.3.tgz",
8527
+ "integrity": "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg==",
8528
+ "license": "MIT",
8529
+ "peer": true,
8530
+ "dependencies": {
8531
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
8532
+ "@babel/plugin-transform-class-properties": "^7.27.1",
8533
+ "@babel/plugin-transform-classes": "^7.28.4",
8534
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
8535
+ "@babel/plugin-transform-optional-chaining": "^7.27.1",
8536
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
8537
+ "@babel/plugin-transform-template-literals": "^7.27.1",
8538
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
8539
+ "@babel/preset-typescript": "^7.27.1",
8540
+ "convert-source-map": "^2.0.0",
8541
+ "semver": "^7.7.3"
8542
+ },
8543
+ "peerDependencies": {
8544
+ "@babel/core": "*",
8545
+ "@react-native/metro-config": "*",
8546
+ "react": "*",
8547
+ "react-native": "0.81 - 0.85"
8548
+ }
8549
+ },
8550
  "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
8551
  "version": "0.81.5",
8552
  "license": "MIT",
keystone-app/package.json CHANGED
@@ -26,6 +26,7 @@
26
  "react-dom": "19.1.0",
27
  "react-native": "0.81.5",
28
  "react-native-gesture-handler": "~2.28.0",
 
29
  "react-native-safe-area-context": "~5.6.0",
30
  "react-native-screens": "~4.16.0",
31
  "react-native-web": "^0.21.2"
 
26
  "react-dom": "19.1.0",
27
  "react-native": "0.81.5",
28
  "react-native-gesture-handler": "~2.28.0",
29
+ "react-native-reanimated": "~4.1.1",
30
  "react-native-safe-area-context": "~5.6.0",
31
  "react-native-screens": "~4.16.0",
32
  "react-native-web": "^0.21.2"