@momo-kits/foundation 0.162.2-test.1 → 0.162.2-test.20

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.
@@ -1,7 +1,12 @@
1
- import React, { Ref, useContext, useEffect, useRef } from 'react';
1
+ import React, { Ref, useContext } from 'react';
2
2
  import LinearGradient from 'react-native-linear-gradient';
3
+ import { Dimensions, Platform, StyleSheet, View } from 'react-native';
4
+ import Animated, {
5
+ Extrapolation,
6
+ interpolate,
7
+ useAnimatedStyle,
8
+ } from 'react-native-reanimated';
3
9
  import { ApplicationContext, MiniAppContext } from '../../Context';
4
- import { Animated, Dimensions, Platform, StyleSheet, View } from 'react-native';
5
10
  import { HeaderType } from '../../Layout/types';
6
11
  import { InputRef, InputSearch } from '../../Input';
7
12
  import Navigation from '../Navigation';
@@ -9,20 +14,19 @@ import { Colors, Radius, Spacing } from '../../Consts';
9
14
  import { Image } from '../../Image';
10
15
  import { SearchHeaderProps } from '../types';
11
16
  import BackgroundImageView from './BackgroundImageView';
17
+ import {
18
+ AnimatedCompatValue,
19
+ useNormalizedSharedValue,
20
+ } from '../utils';
12
21
 
13
22
  const SCREEN_PADDING = 12;
14
23
  const BACK_WIDTH = 28;
15
24
 
16
25
  const { width: screenWidth } = Dimensions.get('window');
17
- const LinearGradientAnimated = Animated.createAnimatedComponent(LinearGradient);
18
26
 
19
- /**
20
- * Header extended with background image
21
- * @constructor
22
- */
23
27
  const HeaderExtendHeader: React.FC<{
24
28
  headerType?: HeaderType;
25
- animatedValue: Animated.Value;
29
+ animatedValue: AnimatedCompatValue;
26
30
  heightHeader: number;
27
31
  headerRightWidth: number;
28
32
  inputSearchProps?: SearchHeaderProps;
@@ -38,118 +42,131 @@ const HeaderExtendHeader: React.FC<{
38
42
  headerRightWidth = 73,
39
43
  inputSearchProps,
40
44
  inputSearchRef,
41
- useShadowHeader = true,
45
+ useShadowHeader: useShadowHeaderProp = true,
42
46
  gradientColor: customGradientColor,
43
47
  headerBackground: customBackground,
44
48
  }) => {
45
49
  const { theme } = useContext(ApplicationContext);
46
50
  const context = useContext<any>(MiniAppContext);
47
- const animated = useRef(new Animated.Value(0));
51
+ const sharedValue = useNormalizedSharedValue(animatedValue);
48
52
  const gradientColor = customGradientColor ?? theme.colors.gradient;
49
53
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
50
54
  const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
51
55
 
52
- const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
56
+ let useShadowHeader = useShadowHeaderProp;
57
+ if (inputSearchProps && Platform.OS === 'android') {
58
+ useShadowHeader = false;
59
+ }
53
60
 
54
- const opacityBackground = animatedValue?.interpolate({
55
- inputRange: [0, 52],
56
- outputRange: [0, 1],
57
- extrapolate: 'clamp',
58
- });
59
- const opacityGradient = animatedValue?.interpolate({
60
- inputRange: [0, 52],
61
- outputRange: [1, 0],
62
- extrapolate: 'clamp',
63
- });
61
+ const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
64
62
 
65
- useEffect(() => {
66
- const listener = animatedValue.addListener(({ value }) => {
67
- animated.current.setValue(value);
68
- });
69
- return () => {
70
- animatedValue?.removeListener(listener);
71
- };
72
- }, [animatedValue]);
63
+ const heightStyle = useAnimatedStyle(() => ({
64
+ height: interpolate(
65
+ sharedValue.value,
66
+ [0, 100],
67
+ [heightHeader + 52, heightHeader],
68
+ Extrapolation.CLAMP,
69
+ ),
70
+ }));
73
71
 
74
- const height = animated.current.interpolate({
75
- inputRange: [0, 100],
76
- outputRange: [heightHeader + 52, heightHeader],
77
- extrapolate: 'clamp',
78
- });
72
+ const gradientOpacityStyle = useAnimatedStyle(() => ({
73
+ opacity: interpolate(sharedValue.value, [0, 52], [1, 0], Extrapolation.CLAMP),
74
+ }));
79
75
 
80
- const translateX = animated.current.interpolate({
81
- inputRange: [0, 100],
82
- outputRange: [SCREEN_PADDING, leftPosition],
83
- extrapolate: 'clamp',
84
- });
85
-
86
- const backgroundColor = animated.current.interpolate({
87
- inputRange: [0, 100],
88
- outputRange: [
89
- theme.colors.background.surface,
90
- theme.colors.background.default,
76
+ const searchTranslateStyle = useAnimatedStyle(() => ({
77
+ transform: [
78
+ {
79
+ translateX: interpolate(
80
+ sharedValue.value,
81
+ [0, 100],
82
+ [SCREEN_PADDING, leftPosition],
83
+ Extrapolation.CLAMP,
84
+ ),
85
+ },
91
86
  ],
92
- extrapolate: 'clamp',
87
+ width: interpolate(
88
+ sharedValue.value,
89
+ [0, 100],
90
+ [
91
+ screenWidth - SCREEN_PADDING * 2,
92
+ screenWidth - leftPosition - 12 - headerRightWidth,
93
+ ],
94
+ Extrapolation.CLAMP,
95
+ ),
96
+ }));
97
+
98
+ const searchBackgroundStyle = useAnimatedStyle(() => {
99
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
100
+ return { backgroundColor: t === 0
101
+ ? theme.colors.background.surface
102
+ : theme.colors.background.default };
93
103
  });
94
104
 
95
- if (inputSearchProps && Platform.OS === 'android') {
96
- useShadowHeader = false;
97
- }
105
+ const extendedHeightStyle = useAnimatedStyle(() => ({
106
+ height: interpolate(
107
+ sharedValue.value,
108
+ [0, 100],
109
+ [heightHeader + 52, heightHeader],
110
+ Extrapolation.CLAMP,
111
+ ),
112
+ }));
98
113
 
99
114
  if (inputSearchProps) {
100
115
  return (
101
116
  <View style={[{ zIndex: 0 }, showBaseLineDebug && styles.debugBaseLine]}>
102
- <Animated.View style={{ height: height }} />
117
+ <Animated.View style={heightStyle} />
103
118
  <BackgroundImageView
104
119
  useShadowHeader={useShadowHeader}
105
120
  heightHeader={heightHeader}
106
- opacityBackground={opacityBackground}
121
+ animatedValue={sharedValue}
107
122
  headerBackground={headerBackground}
108
123
  />
109
124
  <Animated.View
110
125
  style={[
111
126
  styles.headerBox,
112
- { height: headerType === 'extended' ? height : heightHeader },
127
+ headerType === 'extended'
128
+ ? heightStyle
129
+ : { height: heightHeader },
113
130
  ]}
114
131
  >
115
132
  {!!gradientColor && (
116
- <LinearGradientAnimated
117
- colors={[gradientColor, gradientColor + '00']}
118
- style={[styles.extendedHeader, { opacity: opacityGradient }]}
133
+ <Animated.View
134
+ style={[styles.extendedHeader, gradientOpacityStyle]}
119
135
  >
120
- {!!theme.assets?.headerBackground && (
121
- <Image
122
- style={styles.headerBackground}
123
- source={{ uri: theme.assets?.headerBackground }}
124
- loading={false}
125
- />
126
- )}
127
- </LinearGradientAnimated>
136
+ <LinearGradient
137
+ colors={[gradientColor, gradientColor + '00']}
138
+ style={StyleSheet.absoluteFill}
139
+ >
140
+ {!!theme.assets?.headerBackground && (
141
+ <Image
142
+ style={styles.headerBackground}
143
+ source={{ uri: theme.assets?.headerBackground }}
144
+ loading={false}
145
+ />
146
+ )}
147
+ </LinearGradient>
148
+ </Animated.View>
128
149
  )}
129
150
  </Animated.View>
130
151
  <Animated.View
131
- style={{
132
- justifyContent: 'flex-end',
133
- height,
134
- position: 'absolute',
135
- zIndex: 2,
136
- }}
152
+ style={[
153
+ {
154
+ justifyContent: 'flex-end',
155
+ position: 'absolute',
156
+ zIndex: 2,
157
+ },
158
+ heightStyle,
159
+ ]}
137
160
  >
138
161
  <Animated.View
139
- style={{
140
- transform: [{ translateX }],
141
- marginVertical: Spacing.S,
142
- width: animated.current.interpolate({
143
- inputRange: [0, 100],
144
- outputRange: [
145
- screenWidth - SCREEN_PADDING * 2,
146
- screenWidth - leftPosition - 12 - headerRightWidth,
147
- ],
148
- extrapolate: 'clamp',
149
- }),
150
- }}
162
+ style={[{ marginVertical: Spacing.S }, searchTranslateStyle]}
151
163
  >
152
- <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
164
+ <Animated.View
165
+ style={[
166
+ { borderRadius: Radius.XL },
167
+ searchBackgroundStyle,
168
+ ]}
169
+ >
153
170
  <InputSearch
154
171
  {...inputSearchProps}
155
172
  ref={inputSearchRef}
@@ -169,22 +186,26 @@ const HeaderExtendHeader: React.FC<{
169
186
  <BackgroundImageView
170
187
  useShadowHeader={useShadowHeader}
171
188
  heightHeader={heightHeader}
172
- opacityBackground={opacityBackground}
189
+ animatedValue={sharedValue}
173
190
  headerBackground={headerBackground}
174
191
  />
175
192
  {!!gradientColor && (
176
- <LinearGradientAnimated
177
- colors={[gradientColor, gradientColor + '00']}
178
- style={[styles.extendedHeader, { opacity: opacityGradient }]}
193
+ <Animated.View
194
+ style={[styles.extendedHeader, gradientOpacityStyle, extendedHeightStyle]}
179
195
  >
180
- {!!headerBackground && (
181
- <Image
182
- style={styles.headerBackground}
183
- source={{ uri: headerBackground }}
184
- loading={false}
185
- />
186
- )}
187
- </LinearGradientAnimated>
196
+ <LinearGradient
197
+ colors={[gradientColor, gradientColor + '00']}
198
+ style={StyleSheet.absoluteFill}
199
+ >
200
+ {!!headerBackground && (
201
+ <Image
202
+ style={styles.headerBackground}
203
+ source={{ uri: headerBackground }}
204
+ loading={false}
205
+ />
206
+ )}
207
+ </LinearGradient>
208
+ </Animated.View>
188
209
  )}
189
210
  <View style={{ height: heightHeader }} />
190
211
  </View>
@@ -1,11 +1,15 @@
1
1
  import React, { useContext } from 'react';
2
2
  import {
3
- Animated,
4
3
  Dimensions,
5
4
  StyleSheet,
6
5
  TouchableOpacity,
7
6
  View,
8
7
  } from 'react-native';
8
+ import Animated, {
9
+ Extrapolation,
10
+ interpolate,
11
+ useAnimatedStyle,
12
+ } from 'react-native-reanimated';
9
13
  import { ApplicationContext, MiniAppContext } from '../../Context';
10
14
  import { exportFontFamily, Text, useScaleSize } from '../../Text';
11
15
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
@@ -17,22 +21,51 @@ import {
17
21
  import { Image } from '../../Image';
18
22
  import { Icon } from '../../Icon';
19
23
  import { Skeleton } from '../../Skeleton';
24
+ import {
25
+ AnimatedCompatValue,
26
+ useNormalizedSharedValue,
27
+ } from '../utils';
28
+
29
+ type HeaderTitleInterpolate = {
30
+ inputRange: number[];
31
+ outputRange: number[];
32
+ };
33
+
34
+ type HeaderTitleExtraProps = {
35
+ animatedValue?: AnimatedCompatValue;
36
+ interpolate?: HeaderTitleInterpolate;
37
+ tintColor?: string;
38
+ children?: React.ReactNode;
39
+ };
20
40
 
21
41
  /**
22
42
  * default header title used for nav
23
43
  */
24
- const HeaderTitle: React.FC<any> = props => {
44
+ const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = props => {
25
45
  const context = useContext<any>(MiniAppContext);
26
46
 
27
47
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
28
48
 
29
- const opacity = props.animatedValue?.interpolate(
30
- props.interpolate ?? {
31
- inputRange: [0, 200],
32
- outputRange: [0, 1],
33
- extrapolate: 'clamp',
34
- },
35
- );
49
+ const interpolateConfig = props.interpolate ?? {
50
+ inputRange: [0, 200],
51
+ outputRange: [0, 1],
52
+ };
53
+ const hasAnimated = props.animatedValue != null;
54
+ const sharedValue = useNormalizedSharedValue(props.animatedValue);
55
+
56
+ const animatedStyle = useAnimatedStyle(() => {
57
+ if (!hasAnimated) {
58
+ return { opacity: 1 };
59
+ }
60
+ return {
61
+ opacity: interpolate(
62
+ sharedValue.value,
63
+ interpolateConfig.inputRange,
64
+ interpolateConfig.outputRange,
65
+ Extrapolation.CLAMP,
66
+ ),
67
+ };
68
+ });
36
69
 
37
70
  return (
38
71
  <View
@@ -57,9 +90,9 @@ const HeaderTitle: React.FC<any> = props => {
57
90
  },
58
91
  {
59
92
  fontFamily: exportFontFamily('bold', 'action_xs_bold'),
60
- opacity,
61
93
  color: props.tintColor,
62
94
  },
95
+ animatedStyle,
63
96
  ]}
64
97
  numberOfLines={1}
65
98
  />
@@ -1,16 +1,17 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
2
  import { InputRef, InputSearch } from '../../Input';
3
3
  import {
4
- Animated,
5
4
  Dimensions,
6
5
  StyleSheet,
7
6
  TouchableOpacity,
8
7
  View,
9
8
  } from 'react-native';
10
- import React, { useContext, useEffect, useRef } from 'react';
9
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
10
+ import React, { useContext } from 'react';
11
11
  import { SearchHeaderProps } from '../types';
12
12
  import { ApplicationContext, MiniAppContext } from '../../Context';
13
13
  import { Text } from '../../Text';
14
+ import { useNormalizedSharedValue } from '../utils';
14
15
 
15
16
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
16
17
  (
@@ -28,28 +29,17 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
28
29
  const BACK_WIDTH = 28;
29
30
  const { width: screenWidth } = Dimensions.get('window');
30
31
 
31
- const animated = useRef(new Animated.Value(0));
32
+ const sharedValue = useNormalizedSharedValue(animatedValue);
32
33
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
33
34
  const headerRightWidth = props?.headerRightWidth ?? 73;
34
35
 
35
- useEffect(() => {
36
- const listener = animatedValue?.addListener(({ value }) => {
37
- animated.current.setValue(value);
38
- });
39
- return () => {
40
- if (listener) {
41
- animatedValue?.removeListener(listener);
42
- }
36
+ const backgroundStyle = useAnimatedStyle(() => {
37
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
38
+ return {
39
+ backgroundColor: t === 0
40
+ ? theme.colors.background.surface
41
+ : theme.colors.background.default,
43
42
  };
44
- }, [animatedValue]);
45
-
46
- const backgroundColor = animated.current?.interpolate({
47
- inputRange: [0, 100],
48
- outputRange: [
49
- theme.colors.background.surface,
50
- theme.colors.background.default,
51
- ],
52
- extrapolate: 'clamp',
53
43
  });
54
44
 
55
45
  const goBack = () => {
@@ -91,7 +81,9 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
91
81
  },
92
82
  ]}
93
83
  >
94
- <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
84
+ <Animated.View
85
+ style={[{ borderRadius: Radius.XL }, backgroundStyle]}
86
+ >
95
87
  <InputSearch
96
88
  ref={ref}
97
89
  {...props}
@@ -19,7 +19,7 @@ import Navigator from './Navigator';
19
19
  import ScaleSizeProvider from './ScaleSizeProvider';
20
20
  import { getModalOptions, getStackOptions } from './utils';
21
21
  import { NavigationContainerProps } from './types';
22
- import { ApplicationContext, MiniAppContext } from '../Context';
22
+ import { ApplicationContext, FontScaleConfig, MiniAppContext } from '../Context';
23
23
  import Localize from './Localize';
24
24
  import { Colors, defaultTheme } from '../Consts';
25
25
  import { HeaderLeft } from './Components/HeaderLeft';
@@ -29,6 +29,8 @@ import { HeaderBackground } from './Components/HeaderBackground';
29
29
 
30
30
  const Stack = createStackNavigator();
31
31
 
32
+ const FONT_SCALE_OBSERVER_KEY = 'font_scale_config';
33
+
32
34
  const NavigationContainer: React.FC<NavigationContainerProps> = ({
33
35
  screen,
34
36
  theme = defaultTheme,
@@ -45,6 +47,9 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
45
47
  }) => {
46
48
  const context = useContext<any>(MiniAppContext);
47
49
  const [currentContext, setCurrentContext] = useState<any>({});
50
+ const [observerFontScaleConfig, setObserverFontScaleConfig] = useState<
51
+ FontScaleConfig | undefined
52
+ >(context?.fontScaleConfig);
48
53
 
49
54
  const mergedContext = {
50
55
  ...context,
@@ -55,10 +60,24 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
55
60
  },
56
61
  };
57
62
 
63
+ useEffect(() => {
64
+ const sub = maxApi?.observer?.(
65
+ FONT_SCALE_OBSERVER_KEY,
66
+ (data: FontScaleConfig) => {
67
+ setObserverFontScaleConfig(data ?? undefined);
68
+ },
69
+ );
70
+ return () => {
71
+ sub?.remove?.();
72
+ };
73
+ }, [maxApi]);
74
+
58
75
  return (
59
76
  <SafeAreaProvider>
60
77
  <MiniAppContext.Provider value={mergedContext}>
61
- <ScaleSizeProvider scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}>
78
+ <ScaleSizeProvider
79
+ fontScaleConfig={observerFontScaleConfig}
80
+ >
62
81
  <Navigation
63
82
  screen={screen}
64
83
  theme={theme}
@@ -1,13 +1,15 @@
1
- import React, { FC } from 'react';
1
+ import React, { FC, useContext } from 'react';
2
2
  import { ScaleSizeContext } from '../Context';
3
3
  import { ScaleSizeProviderProps } from './types';
4
4
 
5
5
  const ScaleSizeProvider: FC<ScaleSizeProviderProps> = ({
6
- scaleSizeMaxRate,
6
+ fontScaleConfig,
7
7
  children,
8
8
  }) => {
9
+ // Inherit the config from an upper layer (e.g. app root) when the host doesn't set it via MiniAppContext.
10
+ const parent = useContext(ScaleSizeContext);
9
11
  return (
10
- <ScaleSizeContext.Provider value={{ scaleSizeMaxRate }}>
12
+ <ScaleSizeContext.Provider value={fontScaleConfig ?? parent}>
11
13
  {children}
12
14
  </ScaleSizeContext.Provider>
13
15
  );
@@ -78,7 +78,7 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
78
78
  <SafeAreaProvider>
79
79
  <MiniAppContext.Provider value={mergedContext}>
80
80
  <ScaleSizeProvider
81
- scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}
81
+ fontScaleConfig={mergedContext?.fontScaleConfig}
82
82
  >
83
83
  <ApplicationContext.Provider
84
84
  value={{
@@ -7,8 +7,10 @@ import {
7
7
  ViewProps,
8
8
  ViewStyle,
9
9
  } from 'react-native';
10
+ import type { SharedValue } from 'react-native-reanimated';
10
11
  import { PopupNotifyProps } from '../Popup/types';
11
12
  import { InputRef, InputSearchProps } from '../Input';
13
+ import { FontScaleConfig } from '../Context';
12
14
  import Navigation from './Navigation';
13
15
 
14
16
  export type NavigationProps = {
@@ -108,7 +110,7 @@ export type Theme = {
108
110
  };
109
111
 
110
112
  export type ScaleSizeProviderProps = {
111
- scaleSizeMaxRate?: number;
113
+ fontScaleConfig?: FontScaleConfig;
112
114
  children: ViewProps['children'];
113
115
  };
114
116
 
@@ -258,7 +260,7 @@ export interface HeaderBackProps extends NavigationButtonProps {
258
260
  }
259
261
 
260
262
  export type HeaderBackgroundProps = {
261
- animatedValue?: Animated.Value;
263
+ animatedValue?: Animated.Value | SharedValue<number>;
262
264
  useGradient?: boolean;
263
265
  useShadowHeader?: boolean;
264
266
  backgroundColor?: string;
@@ -298,7 +300,7 @@ export type TitleJourneyProps = {
298
300
  };
299
301
 
300
302
  export interface HeaderAnimatedProps extends ViewProps {
301
- animatedValue: Animated.Value;
303
+ animatedValue: Animated.Value | SharedValue<number>;
302
304
  image: string;
303
305
  useScale?: boolean;
304
306
  }
@@ -342,7 +344,7 @@ export type AnimatedHeader = {
342
344
 
343
345
  export interface SearchHeaderProps extends InputSearchProps {
344
346
  ref?: React.RefObject<InputRef>;
345
- animatedValue?: Animated.Value;
347
+ animatedValue?: Animated.Value | SharedValue<number>;
346
348
  headerRightWidth?: 0 | 74 | 110 | number;
347
349
  leftPosition?: 12 | 48 | number;
348
350
  renderButtons?: () => ReactNode;
@@ -6,6 +6,8 @@ import {
6
6
  import type { HeaderTitleProps, NavigationOptions } from './types';
7
7
  import { Colors, Spacing } from '../Consts';
8
8
  import { Animated, AppState, Platform } from 'react-native';
9
+ import type { SharedValue } from 'react-native-reanimated';
10
+ import { useSharedValue } from 'react-native-reanimated';
9
11
  import {
10
12
  MiniAppContext,
11
13
  ScreenContext,
@@ -60,7 +62,7 @@ const getModalOptions = (): StackNavigationOptions => {
60
62
  */
61
63
  const getOptions = (
62
64
  params: NavigationOptions,
63
- animatedValue?: Animated.Value,
65
+ animatedValue?: SharedValue<number>,
64
66
  ) => {
65
67
  let options: StackNavigationOptions = {};
66
68
 
@@ -131,7 +133,7 @@ const getOptions = (
131
133
 
132
134
  const exportHeaderTitle = (
133
135
  params: NavigationOptions,
134
- animatedValue?: Animated.Value,
136
+ animatedValue?: SharedValue<number>,
135
137
  ): StackNavigationOptions => {
136
138
  if (typeof params.headerTitle === 'object') {
137
139
  return {
@@ -228,6 +230,43 @@ const useAppState = () => {
228
230
  };
229
231
  };
230
232
 
233
+ /**
234
+ * Union of the two animation runtimes an internal foundation component might
235
+ * receive. Public Screen props still accept React Native Animated.Value only;
236
+ * this type keeps lower-level header/floating components compatible while they
237
+ * render with reanimated.
238
+ */
239
+ export type AnimatedCompatValue = Animated.Value | SharedValue<number>;
240
+
241
+ const isRNAnimatedValue = (v: unknown): v is Animated.Value =>
242
+ !!v &&
243
+ typeof (v as Animated.Value).setValue === 'function' &&
244
+ typeof (v as Animated.Value).interpolate === 'function';
245
+
246
+ const isSharedValue = (v: unknown): v is SharedValue<number> =>
247
+ !!v && !isRNAnimatedValue(v) && 'value' in (v as object);
248
+
249
+ const useNormalizedSharedValue = (
250
+ input?: AnimatedCompatValue,
251
+ ): SharedValue<number> => {
252
+ const fallback = useSharedValue(0);
253
+
254
+ useEffect(() => {
255
+ if (!isRNAnimatedValue(input)) {
256
+ return;
257
+ }
258
+ const id = input.addListener(({ value }) => {
259
+ fallback.value = value;
260
+ });
261
+ return () => input.removeListener(id);
262
+ }, [input, fallback]);
263
+
264
+ if (isSharedValue(input)) {
265
+ return input;
266
+ }
267
+ return fallback;
268
+ };
269
+
231
270
  export {
232
271
  getStackOptions,
233
272
  getModalOptions,
@@ -236,4 +275,5 @@ export {
236
275
  setAutomationID,
237
276
  useComponentId,
238
277
  useAppState,
278
+ useNormalizedSharedValue,
239
279
  };