@impulse-ui-native/flyout 2.3.5 → 2.4.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ This package uses React Native Gesture Handler, Reanimated, Safe Area Context, a
13
13
  ## Main exports
14
14
 
15
15
  - `Flyout` renders the sheet, overlay, drag handle, title, safe-area padding, and lifecycle animations.
16
- - `FlyoutProps` adds `open`, `placement`, `topOffset`, and `bottomOffset` to the common layer contract.
16
+ - `FlyoutProps` adds `placement`, `topOffset`, and `bottomOffset` to the common overlay contract.
17
17
 
18
18
  ## Usage
19
19
 
@@ -33,4 +33,4 @@ import { Flyout } from "@impulse-ui-native/flyout";
33
33
 
34
34
  The overlay closes the sheet when pressed. A drag past half the measured sheet height, or a sufficiently fast swipe toward the edge, closes it as well.
35
35
 
36
- For app-wide imperative sheets, register flyouts through `@impulse-ui-native/layers` and mount `LayerCenter` once.
36
+ For app-wide imperative sheets, register `Flyout` through an `OverlayStore` and mount `OverlayHost` inside the matching `OverlayProvider`.
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { PropsWithChildren } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { FlyoutProps } from '../types/flyout.types.mjs';
5
- import '@impulse-ui-native/types';
5
+ import '@impulse-ui-native/overlay';
6
6
 
7
7
  declare const Flyout: react.MemoExoticComponent<(props: PropsWithChildren<FlyoutProps>) => react_jsx_runtime.JSX.Element | null>;
8
8
 
@@ -1,46 +1,17 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import {
3
- Fragment,
4
- memo,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- useRef,
9
- useState
10
- } from "react";
11
- import {
12
- Pressable,
13
- StyleSheet,
14
- useWindowDimensions
15
- } from "react-native";
16
- import { Gesture, GestureDetector } from "react-native-gesture-handler";
17
- import Animated, {
18
- Easing,
19
- useAnimatedStyle,
20
- useSharedValue,
21
- withSpring,
22
- withTiming
23
- } from "react-native-reanimated";
2
+ import { Fragment, memo, useMemo } from "react";
3
+ import { Pressable, StyleSheet, useWindowDimensions } from "react-native";
4
+ import { GestureDetector } from "react-native-gesture-handler";
5
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
24
6
  import {
25
7
  SafeAreaView,
26
8
  useSafeAreaInsets
27
9
  } from "react-native-safe-area-context";
28
- import { scheduleOnRN } from "react-native-worklets";
29
10
  import { View } from "@impulse-ui-native/primitives";
30
11
  import { useTheme } from "@impulse-ui-native/theme";
31
- import { FlyoutHandleHeight } from "../constants";
12
+ import { useFlyoutLifecycle } from "../hooks";
32
13
  import { FlyoutHandle } from "./flyout-handle";
33
14
  import { FlyoutTitle } from "./flyout-title";
34
- const EnterAnimationConfig = {
35
- damping: 50,
36
- stiffness: 300,
37
- mass: 1,
38
- overshootClamping: true
39
- };
40
- const ExitAnimationConfig = {
41
- duration: 150,
42
- easing: Easing.in(Easing.quad)
43
- };
44
15
  const Flyout = memo(function Flyout2(props) {
45
16
  const {
46
17
  id,
@@ -64,16 +35,27 @@ const Flyout = memo(function Flyout2(props) {
64
35
  const screenHeight = windowDimensions.height;
65
36
  const maxHeight = screenHeight * flyoutTokens.maxHeightRatio;
66
37
  const zIndex = flyoutTokens.zIndexBase + layer;
67
- const [mounted, setMounted] = useState(open);
68
- const [measuredHeight, setMeasuredHeight] = useState(null);
69
- const [hasMeasured, setHasMeasured] = useState(false);
70
- const [isTouchable, setIsTouchable] = useState(false);
71
- const didOpen = useRef(false);
72
- const isClosing = useRef(false);
73
- const translateY = useSharedValue(
74
- placement === "top" ? -screenHeight : screenHeight
75
- );
76
- const opacity = useSharedValue(0);
38
+ const {
39
+ close,
40
+ dragGesture,
41
+ hasMeasured,
42
+ isTouchable,
43
+ mounted,
44
+ onLayout,
45
+ opacity,
46
+ translateY
47
+ } = useFlyoutLifecycle({
48
+ id,
49
+ open,
50
+ placement,
51
+ screenHeight,
52
+ safeAreaInset: insets[placement],
53
+ overlayVisibleOpacity: flyoutTokens.overlayVisibleOpacity,
54
+ onClose,
55
+ onCloseFinished,
56
+ onOpen,
57
+ onOpenFinished
58
+ });
77
59
  const animatedStyle = useAnimatedStyle(() => {
78
60
  const offset = placement === "bottom" ? -(placementOffset ?? 0) : placementOffset ?? 0;
79
61
  return {
@@ -92,131 +74,6 @@ const Flyout = memo(function Flyout2(props) {
92
74
  const styles = useMemo(() => {
93
75
  return themedStyles(theme, placement, maxHeight);
94
76
  }, [theme, placement, maxHeight]);
95
- const onLayout = useCallback(
96
- (event) => {
97
- if (measuredHeight !== null) {
98
- return;
99
- }
100
- const rawHeight = event.nativeEvent.layout.height;
101
- const clampedHeight = rawHeight + insets[placement] + FlyoutHandleHeight;
102
- translateY.value = placement === "top" ? -clampedHeight : clampedHeight;
103
- setMeasuredHeight(clampedHeight);
104
- setHasMeasured(true);
105
- },
106
- [insets, measuredHeight, placement, translateY]
107
- );
108
- const animateOut = useCallback(() => {
109
- if (measuredHeight === null || isClosing.current) {
110
- return;
111
- }
112
- isClosing.current = true;
113
- setIsTouchable(false);
114
- onClose?.(id);
115
- const target = placement === "top" ? -measuredHeight : measuredHeight;
116
- translateY.value = withTiming(target, ExitAnimationConfig, (finished) => {
117
- if (finished) {
118
- if (onCloseFinished) {
119
- scheduleOnRN(onCloseFinished, id);
120
- }
121
- scheduleOnRN(setMounted, false);
122
- scheduleOnRN(setMeasuredHeight, null);
123
- scheduleOnRN(setHasMeasured, false);
124
- }
125
- });
126
- opacity.value = withTiming(0, ExitAnimationConfig);
127
- didOpen.current = false;
128
- }, [
129
- id,
130
- measuredHeight,
131
- onClose,
132
- onCloseFinished,
133
- opacity,
134
- placement,
135
- translateY
136
- ]);
137
- const dragGesture = useMemo(
138
- () => Gesture.Pan().enabled(hasMeasured).onUpdate((event) => {
139
- if (placement === "bottom") {
140
- translateY.value = Math.max(0, event.translationY);
141
- return;
142
- }
143
- translateY.value = Math.min(0, event.translationY);
144
- }).onEnd((event) => {
145
- if (measuredHeight === null) {
146
- return;
147
- }
148
- let shouldClose = false;
149
- if (placement === "bottom") {
150
- shouldClose = event.translationY > measuredHeight * 0.5 || event.velocityY > 1500;
151
- }
152
- if (placement === "top") {
153
- shouldClose = event.translationY < -measuredHeight * 0.5 || event.velocityY < -1500;
154
- }
155
- if (shouldClose) {
156
- scheduleOnRN(animateOut);
157
- return;
158
- }
159
- translateY.value = withSpring(0, EnterAnimationConfig);
160
- opacity.value = withSpring(
161
- flyoutTokens.overlayVisibleOpacity,
162
- EnterAnimationConfig
163
- );
164
- }),
165
- [
166
- animateOut,
167
- flyoutTokens.overlayVisibleOpacity,
168
- hasMeasured,
169
- measuredHeight,
170
- opacity,
171
- placement,
172
- translateY
173
- ]
174
- );
175
- useEffect(() => {
176
- if (!open) {
177
- return;
178
- }
179
- setMounted(true);
180
- setIsTouchable(false);
181
- setMeasuredHeight(null);
182
- setHasMeasured(false);
183
- isClosing.current = false;
184
- didOpen.current = false;
185
- translateY.value = placement === "top" ? -screenHeight : screenHeight;
186
- opacity.value = 0;
187
- }, [open, opacity, placement, screenHeight, translateY]);
188
- useEffect(() => {
189
- if (!open || measuredHeight === null || !hasMeasured || didOpen.current) {
190
- return;
191
- }
192
- setIsTouchable(true);
193
- onOpen?.(id);
194
- translateY.value = withSpring(0, EnterAnimationConfig, (finished) => {
195
- if (finished && onOpenFinished) {
196
- scheduleOnRN(onOpenFinished, id);
197
- }
198
- });
199
- opacity.value = withSpring(
200
- flyoutTokens.overlayVisibleOpacity,
201
- EnterAnimationConfig
202
- );
203
- didOpen.current = true;
204
- }, [
205
- flyoutTokens.overlayVisibleOpacity,
206
- hasMeasured,
207
- id,
208
- measuredHeight,
209
- onOpen,
210
- onOpenFinished,
211
- opacity,
212
- open,
213
- translateY
214
- ]);
215
- useEffect(() => {
216
- if (!open && didOpen.current) {
217
- animateOut();
218
- }
219
- }, [animateOut, open]);
220
77
  if (!mounted) {
221
78
  return null;
222
79
  }
@@ -226,7 +83,7 @@ const Flyout = memo(function Flyout2(props) {
226
83
  {
227
84
  pointerEvents: isTouchable ? "auto" : "none",
228
85
  style: [StyleSheet.absoluteFill, overlayStyle],
229
- children: /* @__PURE__ */ jsx(Pressable, { onPress: animateOut, style: StyleSheet.absoluteFill })
86
+ children: /* @__PURE__ */ jsx(Pressable, { onPress: close, style: StyleSheet.absoluteFill })
230
87
  }
231
88
  ),
232
89
  /* @__PURE__ */ jsx(GestureDetector, { gesture: dragGesture, children: /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/flyout.tsx"],"sourcesContent":["import {\n Fragment,\n memo,\n PropsWithChildren,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n LayoutChangeEvent,\n Pressable,\n StyleSheet,\n useWindowDimensions,\n} from \"react-native\";\nimport { Gesture, GestureDetector } from \"react-native-gesture-handler\";\nimport Animated, {\n Easing,\n useAnimatedStyle,\n useSharedValue,\n withSpring,\n withTiming,\n} from \"react-native-reanimated\";\nimport {\n Edge,\n SafeAreaView,\n useSafeAreaInsets,\n} from \"react-native-safe-area-context\";\nimport { scheduleOnRN } from \"react-native-worklets\";\n\nimport { View } from \"@impulse-ui-native/primitives\";\nimport { AppTheme, useTheme } from \"@impulse-ui-native/theme\";\n\nimport { FlyoutHandleHeight } from \"../constants\";\nimport { FlyoutProps } from \"../types\";\nimport { FlyoutHandle } from \"./flyout-handle\";\nimport { FlyoutTitle } from \"./flyout-title\";\n\nconst EnterAnimationConfig = {\n damping: 50,\n stiffness: 300,\n mass: 1,\n overshootClamping: true,\n};\n\nconst ExitAnimationConfig = {\n duration: 150,\n easing: Easing.in(Easing.quad),\n};\n\nexport const Flyout = memo(function Flyout(\n props: PropsWithChildren<FlyoutProps>,\n) {\n const {\n id,\n children,\n placement = \"bottom\",\n layer = 0,\n title,\n open,\n topOffset,\n bottomOffset,\n onCloseFinished,\n onClose,\n onOpen,\n onOpenFinished,\n } = props;\n\n const theme = useTheme();\n const insets = useSafeAreaInsets();\n\n const windowDimensions = useWindowDimensions();\n\n const flyoutTokens = theme.components.flyout;\n\n const placementOffset = placement === \"top\" ? topOffset : bottomOffset;\n const screenHeight = windowDimensions.height;\n const maxHeight = screenHeight * flyoutTokens.maxHeightRatio;\n const zIndex = flyoutTokens.zIndexBase + layer;\n\n const [mounted, setMounted] = useState(open);\n const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);\n const [hasMeasured, setHasMeasured] = useState(false);\n const [isTouchable, setIsTouchable] = useState(false);\n\n const didOpen = useRef(false);\n const isClosing = useRef(false);\n\n const translateY = useSharedValue(\n placement === \"top\" ? -screenHeight : screenHeight,\n );\n const opacity = useSharedValue(0);\n\n const animatedStyle = useAnimatedStyle(() => {\n const offset =\n placement === \"bottom\" ? -(placementOffset ?? 0) : (placementOffset ?? 0);\n\n return {\n transform: [{ translateY: translateY.value + offset }],\n zIndex,\n };\n });\n\n const overlayStyle = useAnimatedStyle(() => ({\n opacity: opacity.value,\n zIndex,\n backgroundColor: flyoutTokens.overlayColor,\n }));\n\n const edges = useMemo<Edge[]>(() => {\n return [placement, \"left\", \"right\"];\n }, [placement]);\n\n const styles = useMemo(() => {\n return themedStyles(theme, placement, maxHeight);\n }, [theme, placement, maxHeight]);\n\n const onLayout = useCallback(\n (event: LayoutChangeEvent) => {\n if (measuredHeight !== null) {\n return;\n }\n\n const rawHeight = event.nativeEvent.layout.height;\n const clampedHeight = rawHeight + insets[placement] + FlyoutHandleHeight;\n\n translateY.value = placement === \"top\" ? -clampedHeight : clampedHeight;\n\n setMeasuredHeight(clampedHeight);\n setHasMeasured(true);\n },\n [insets, measuredHeight, placement, translateY],\n );\n\n const animateOut = useCallback(() => {\n if (measuredHeight === null || isClosing.current) {\n return;\n }\n\n isClosing.current = true;\n setIsTouchable(false);\n onClose?.(id);\n\n const target = placement === \"top\" ? -measuredHeight : measuredHeight;\n\n translateY.value = withTiming(target, ExitAnimationConfig, (finished) => {\n if (finished) {\n if (onCloseFinished) {\n scheduleOnRN(onCloseFinished, id);\n }\n\n scheduleOnRN(setMounted, false);\n scheduleOnRN(setMeasuredHeight, null);\n scheduleOnRN(setHasMeasured, false);\n }\n });\n\n opacity.value = withTiming(0, ExitAnimationConfig);\n didOpen.current = false;\n }, [\n id,\n measuredHeight,\n onClose,\n onCloseFinished,\n opacity,\n placement,\n translateY,\n ]);\n\n const dragGesture = useMemo(\n () =>\n Gesture.Pan()\n .enabled(hasMeasured)\n .onUpdate((event) => {\n if (placement === \"bottom\") {\n translateY.value = Math.max(0, event.translationY);\n return;\n }\n\n translateY.value = Math.min(0, event.translationY);\n })\n .onEnd((event) => {\n if (measuredHeight === null) {\n return;\n }\n\n let shouldClose = false;\n\n if (placement === \"bottom\") {\n shouldClose =\n event.translationY > measuredHeight * 0.5 ||\n event.velocityY > 1500;\n }\n\n if (placement === \"top\") {\n shouldClose =\n event.translationY < -measuredHeight * 0.5 ||\n event.velocityY < -1500;\n }\n\n if (shouldClose) {\n scheduleOnRN(animateOut);\n return;\n }\n\n translateY.value = withSpring(0, EnterAnimationConfig);\n opacity.value = withSpring(\n flyoutTokens.overlayVisibleOpacity,\n EnterAnimationConfig,\n );\n }),\n [\n animateOut,\n flyoutTokens.overlayVisibleOpacity,\n hasMeasured,\n measuredHeight,\n opacity,\n placement,\n translateY,\n ],\n );\n\n useEffect(() => {\n if (!open) {\n return;\n }\n\n setMounted(true);\n setIsTouchable(false);\n setMeasuredHeight(null);\n setHasMeasured(false);\n\n isClosing.current = false;\n didOpen.current = false;\n\n translateY.value = placement === \"top\" ? -screenHeight : screenHeight;\n opacity.value = 0;\n }, [open, opacity, placement, screenHeight, translateY]);\n\n useEffect(() => {\n if (!open || measuredHeight === null || !hasMeasured || didOpen.current) {\n return;\n }\n\n setIsTouchable(true);\n onOpen?.(id);\n\n translateY.value = withSpring(0, EnterAnimationConfig, (finished) => {\n if (finished && onOpenFinished) {\n scheduleOnRN(onOpenFinished, id);\n }\n });\n\n opacity.value = withSpring(\n flyoutTokens.overlayVisibleOpacity,\n EnterAnimationConfig,\n );\n\n didOpen.current = true;\n }, [\n flyoutTokens.overlayVisibleOpacity,\n hasMeasured,\n id,\n measuredHeight,\n onOpen,\n onOpenFinished,\n opacity,\n open,\n translateY,\n ]);\n\n useEffect(() => {\n if (!open && didOpen.current) {\n animateOut();\n }\n }, [animateOut, open]);\n\n if (!mounted) {\n return null;\n }\n\n return (\n <Fragment>\n <Animated.View\n pointerEvents={isTouchable ? \"auto\" : \"none\"}\n style={[StyleSheet.absoluteFill, overlayStyle]}\n >\n <Pressable onPress={animateOut} style={StyleSheet.absoluteFill} />\n </Animated.View>\n\n <GestureDetector gesture={dragGesture}>\n <Animated.View\n onLayout={onLayout}\n style={[\n styles.container,\n !hasMeasured && styles.hidden,\n animatedStyle,\n ]}\n >\n <SafeAreaView edges={edges}>\n {placement === \"bottom\" ? (\n <FlyoutHandle placement={placement} />\n ) : null}\n\n {title ? <FlyoutTitle title={title} /> : null}\n\n <View style={styles.content}>{children}</View>\n\n {placement === \"top\" ? (\n <FlyoutHandle placement={placement} />\n ) : null}\n </SafeAreaView>\n </Animated.View>\n </GestureDetector>\n </Fragment>\n );\n});\n\nfunction themedStyles(\n theme: AppTheme,\n placement: \"top\" | \"bottom\",\n maxHeight: number,\n) {\n const flyoutTokens = theme.components.flyout;\n\n return StyleSheet.create({\n container: {\n position: \"absolute\",\n [placement]: 0,\n left: 0,\n right: 0,\n backgroundColor: flyoutTokens.backgroundColor,\n borderTopLeftRadius:\n placement === \"bottom\" ? flyoutTokens.borderRadius : 0,\n borderTopRightRadius:\n placement === \"bottom\" ? flyoutTokens.borderRadius : 0,\n borderBottomLeftRadius:\n placement === \"top\" ? flyoutTokens.borderRadius : 0,\n borderBottomRightRadius:\n placement === \"top\" ? flyoutTokens.borderRadius : 0,\n },\n content: {\n paddingHorizontal: flyoutTokens.contentPaddingHorizontal,\n maxHeight,\n },\n hidden: {\n opacity: flyoutTokens.hiddenOpacity,\n },\n });\n}\n"],"mappings":"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","names":["Flyout"]}
1
+ {"version":3,"sources":["../../src/components/flyout.tsx"],"sourcesContent":["import { Fragment, memo, PropsWithChildren, useMemo } from \"react\";\nimport { Pressable, StyleSheet, useWindowDimensions } from \"react-native\";\nimport { GestureDetector } from \"react-native-gesture-handler\";\nimport Animated, { useAnimatedStyle } from \"react-native-reanimated\";\nimport {\n Edge,\n SafeAreaView,\n useSafeAreaInsets,\n} from \"react-native-safe-area-context\";\n\nimport { View } from \"@impulse-ui-native/primitives\";\nimport { AppTheme, useTheme } from \"@impulse-ui-native/theme\";\n\nimport { useFlyoutLifecycle } from \"../hooks\";\nimport { FlyoutProps } from \"../types\";\nimport { FlyoutHandle } from \"./flyout-handle\";\nimport { FlyoutTitle } from \"./flyout-title\";\n\nexport const Flyout = memo(function Flyout(\n props: PropsWithChildren<FlyoutProps>,\n) {\n const {\n id,\n children,\n placement = \"bottom\",\n layer = 0,\n title,\n open,\n topOffset,\n bottomOffset,\n onCloseFinished,\n onClose,\n onOpen,\n onOpenFinished,\n } = props;\n\n const theme = useTheme();\n const insets = useSafeAreaInsets();\n\n const windowDimensions = useWindowDimensions();\n\n const flyoutTokens = theme.components.flyout;\n\n const placementOffset = placement === \"top\" ? topOffset : bottomOffset;\n const screenHeight = windowDimensions.height;\n const maxHeight = screenHeight * flyoutTokens.maxHeightRatio;\n const zIndex = flyoutTokens.zIndexBase + layer;\n\n const {\n close,\n dragGesture,\n hasMeasured,\n isTouchable,\n mounted,\n onLayout,\n opacity,\n translateY,\n } = useFlyoutLifecycle({\n id,\n open,\n placement,\n screenHeight,\n safeAreaInset: insets[placement],\n overlayVisibleOpacity: flyoutTokens.overlayVisibleOpacity,\n onClose,\n onCloseFinished,\n onOpen,\n onOpenFinished,\n });\n\n const animatedStyle = useAnimatedStyle(() => {\n const offset =\n placement === \"bottom\" ? -(placementOffset ?? 0) : (placementOffset ?? 0);\n\n return {\n transform: [{ translateY: translateY.value + offset }],\n zIndex,\n };\n });\n\n const overlayStyle = useAnimatedStyle(() => ({\n opacity: opacity.value,\n zIndex,\n backgroundColor: flyoutTokens.overlayColor,\n }));\n\n const edges = useMemo<Edge[]>(() => {\n return [placement, \"left\", \"right\"];\n }, [placement]);\n\n const styles = useMemo(() => {\n return themedStyles(theme, placement, maxHeight);\n }, [theme, placement, maxHeight]);\n\n if (!mounted) {\n return null;\n }\n\n return (\n <Fragment>\n <Animated.View\n pointerEvents={isTouchable ? \"auto\" : \"none\"}\n style={[StyleSheet.absoluteFill, overlayStyle]}\n >\n <Pressable onPress={close} style={StyleSheet.absoluteFill} />\n </Animated.View>\n\n <GestureDetector gesture={dragGesture}>\n <Animated.View\n onLayout={onLayout}\n style={[\n styles.container,\n !hasMeasured && styles.hidden,\n animatedStyle,\n ]}\n >\n <SafeAreaView edges={edges}>\n {placement === \"bottom\" ? (\n <FlyoutHandle placement={placement} />\n ) : null}\n\n {title ? <FlyoutTitle title={title} /> : null}\n\n <View style={styles.content}>{children}</View>\n\n {placement === \"top\" ? (\n <FlyoutHandle placement={placement} />\n ) : null}\n </SafeAreaView>\n </Animated.View>\n </GestureDetector>\n </Fragment>\n );\n});\n\nfunction themedStyles(\n theme: AppTheme,\n placement: \"top\" | \"bottom\",\n maxHeight: number,\n) {\n const flyoutTokens = theme.components.flyout;\n\n return StyleSheet.create({\n container: {\n position: \"absolute\",\n [placement]: 0,\n left: 0,\n right: 0,\n backgroundColor: flyoutTokens.backgroundColor,\n borderTopLeftRadius:\n placement === \"bottom\" ? flyoutTokens.borderRadius : 0,\n borderTopRightRadius:\n placement === \"bottom\" ? flyoutTokens.borderRadius : 0,\n borderBottomLeftRadius:\n placement === \"top\" ? flyoutTokens.borderRadius : 0,\n borderBottomRightRadius:\n placement === \"top\" ? flyoutTokens.borderRadius : 0,\n },\n content: {\n paddingHorizontal: flyoutTokens.contentPaddingHorizontal,\n maxHeight,\n },\n hidden: {\n opacity: flyoutTokens.hiddenOpacity,\n },\n });\n}\n"],"mappings":"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","names":["Flyout"]}
@@ -2,4 +2,4 @@ export { Flyout } from './flyout.mjs';
2
2
  import 'react';
3
3
  import 'react/jsx-runtime';
4
4
  import '../types/flyout.types.mjs';
5
- import '@impulse-ui-native/types';
5
+ import '@impulse-ui-native/overlay';
@@ -0,0 +1,6 @@
1
+ export { useFlyoutLifecycle } from './use-flyout-lifecycle.hook.mjs';
2
+ import 'react-native-reanimated';
3
+ import 'react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture';
4
+ import 'react-native';
5
+ import '../types/flyout.types.mjs';
6
+ import '@impulse-ui-native/overlay';
@@ -0,0 +1,5 @@
1
+ import { useFlyoutLifecycle } from "./use-flyout-lifecycle.hook";
2
+ export {
3
+ useFlyoutLifecycle
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/index.ts"],"sourcesContent":["export { useFlyoutLifecycle } from \"./use-flyout-lifecycle.hook\";\n"],"mappings":"AAAA,SAAS,0BAA0B;","names":[]}
@@ -0,0 +1,30 @@
1
+ import * as react_native_reanimated from 'react-native-reanimated';
2
+ import * as react_native_gesture_handler_lib_typescript_handlers_gestures_panGesture from 'react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture';
3
+ import { LayoutChangeEvent } from 'react-native';
4
+ import { FlyoutProps } from '../types/flyout.types.mjs';
5
+ import '@impulse-ui-native/overlay';
6
+
7
+ interface UseFlyoutLifecycleProps {
8
+ id: FlyoutProps["id"];
9
+ open: FlyoutProps["open"];
10
+ placement: NonNullable<FlyoutProps["placement"]>;
11
+ screenHeight: number;
12
+ safeAreaInset: number;
13
+ overlayVisibleOpacity: number;
14
+ onClose: FlyoutProps["onClose"];
15
+ onCloseFinished: FlyoutProps["onCloseFinished"];
16
+ onOpen: FlyoutProps["onOpen"];
17
+ onOpenFinished: FlyoutProps["onOpenFinished"];
18
+ }
19
+ declare function useFlyoutLifecycle(props: UseFlyoutLifecycleProps): {
20
+ close: () => void;
21
+ dragGesture: react_native_gesture_handler_lib_typescript_handlers_gestures_panGesture.PanGesture;
22
+ hasMeasured: boolean;
23
+ isTouchable: boolean;
24
+ mounted: boolean | undefined;
25
+ onLayout: (event: LayoutChangeEvent) => void;
26
+ opacity: react_native_reanimated.SharedValue<number>;
27
+ translateY: react_native_reanimated.SharedValue<number>;
28
+ };
29
+
30
+ export { useFlyoutLifecycle };
@@ -0,0 +1,189 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Gesture } from "react-native-gesture-handler";
3
+ import {
4
+ Easing,
5
+ useSharedValue,
6
+ withSpring,
7
+ withTiming
8
+ } from "react-native-reanimated";
9
+ import { scheduleOnRN } from "react-native-worklets";
10
+ import { FlyoutHandleHeight } from "../constants";
11
+ const EnterAnimationConfig = {
12
+ damping: 50,
13
+ stiffness: 300,
14
+ mass: 1,
15
+ overshootClamping: true
16
+ };
17
+ const ExitAnimationConfig = {
18
+ duration: 150,
19
+ easing: Easing.in(Easing.quad)
20
+ };
21
+ function useFlyoutLifecycle(props) {
22
+ const {
23
+ id,
24
+ open,
25
+ placement,
26
+ screenHeight,
27
+ safeAreaInset,
28
+ overlayVisibleOpacity,
29
+ onClose,
30
+ onCloseFinished,
31
+ onOpen,
32
+ onOpenFinished
33
+ } = props;
34
+ const [mounted, setMounted] = useState(open);
35
+ const [measuredHeight, setMeasuredHeight] = useState(null);
36
+ const [isTouchable, setIsTouchable] = useState(false);
37
+ const didOpen = useRef(false);
38
+ const isClosing = useRef(false);
39
+ const transitionId = useRef(0);
40
+ const translateY = useSharedValue(
41
+ placement === "top" ? -screenHeight : screenHeight
42
+ );
43
+ const opacity = useSharedValue(0);
44
+ const hasMeasured = measuredHeight !== null;
45
+ const onLayout = useCallback(
46
+ (event) => {
47
+ if (measuredHeight !== null) {
48
+ return;
49
+ }
50
+ const height = event.nativeEvent.layout.height + safeAreaInset + FlyoutHandleHeight;
51
+ translateY.value = placement === "top" ? -height : height;
52
+ setMeasuredHeight(height);
53
+ },
54
+ [measuredHeight, placement, safeAreaInset, translateY]
55
+ );
56
+ const finishClosing = useCallback(
57
+ (finishedTransitionId) => {
58
+ if (!isClosing.current || transitionId.current !== finishedTransitionId) {
59
+ return;
60
+ }
61
+ isClosing.current = false;
62
+ setMounted(false);
63
+ setMeasuredHeight(null);
64
+ onCloseFinished?.(id);
65
+ },
66
+ [id, onCloseFinished]
67
+ );
68
+ const close = useCallback(() => {
69
+ if (measuredHeight === null || isClosing.current) {
70
+ return;
71
+ }
72
+ isClosing.current = true;
73
+ transitionId.current += 1;
74
+ setIsTouchable(false);
75
+ onClose?.(id);
76
+ const target = placement === "top" ? -measuredHeight : measuredHeight;
77
+ const closingTransitionId = transitionId.current;
78
+ translateY.value = withTiming(target, ExitAnimationConfig, (finished) => {
79
+ if (finished) {
80
+ scheduleOnRN(finishClosing, closingTransitionId);
81
+ }
82
+ });
83
+ opacity.value = withTiming(0, ExitAnimationConfig);
84
+ didOpen.current = false;
85
+ }, [
86
+ finishClosing,
87
+ id,
88
+ measuredHeight,
89
+ onClose,
90
+ opacity,
91
+ placement,
92
+ translateY
93
+ ]);
94
+ const dragGesture = useMemo(
95
+ () => Gesture.Pan().enabled(hasMeasured).onUpdate((event) => {
96
+ if (placement === "bottom") {
97
+ translateY.value = Math.max(0, event.translationY);
98
+ return;
99
+ }
100
+ translateY.value = Math.min(0, event.translationY);
101
+ }).onEnd((event) => {
102
+ if (measuredHeight === null) {
103
+ return;
104
+ }
105
+ const shouldClose = placement === "bottom" ? event.translationY > measuredHeight * 0.5 || event.velocityY > 1500 : event.translationY < -measuredHeight * 0.5 || event.velocityY < -1500;
106
+ if (shouldClose) {
107
+ scheduleOnRN(close);
108
+ return;
109
+ }
110
+ translateY.value = withSpring(0, EnterAnimationConfig);
111
+ opacity.value = withSpring(
112
+ overlayVisibleOpacity,
113
+ EnterAnimationConfig
114
+ );
115
+ }),
116
+ [
117
+ close,
118
+ hasMeasured,
119
+ measuredHeight,
120
+ opacity,
121
+ overlayVisibleOpacity,
122
+ placement,
123
+ translateY
124
+ ]
125
+ );
126
+ useEffect(() => {
127
+ if (!open) {
128
+ return;
129
+ }
130
+ transitionId.current += 1;
131
+ setIsTouchable(false);
132
+ isClosing.current = false;
133
+ didOpen.current = false;
134
+ if (!mounted) {
135
+ setMounted(true);
136
+ setMeasuredHeight(null);
137
+ translateY.value = placement === "top" ? -screenHeight : screenHeight;
138
+ opacity.value = 0;
139
+ }
140
+ }, [mounted, open, opacity, placement, screenHeight, translateY]);
141
+ useEffect(() => {
142
+ if (!open || measuredHeight === null || didOpen.current) {
143
+ return;
144
+ }
145
+ setIsTouchable(true);
146
+ onOpen?.(id);
147
+ translateY.value = withSpring(0, EnterAnimationConfig, (finished) => {
148
+ if (finished && onOpenFinished) {
149
+ scheduleOnRN(onOpenFinished, id);
150
+ }
151
+ });
152
+ opacity.value = withSpring(overlayVisibleOpacity, EnterAnimationConfig);
153
+ didOpen.current = true;
154
+ }, [
155
+ id,
156
+ measuredHeight,
157
+ onOpen,
158
+ onOpenFinished,
159
+ opacity,
160
+ open,
161
+ overlayVisibleOpacity,
162
+ translateY
163
+ ]);
164
+ useEffect(() => {
165
+ if (open || !mounted || isClosing.current) {
166
+ return;
167
+ }
168
+ if (didOpen.current) {
169
+ close();
170
+ return;
171
+ }
172
+ setMounted(false);
173
+ setMeasuredHeight(null);
174
+ }, [close, mounted, open]);
175
+ return {
176
+ close,
177
+ dragGesture,
178
+ hasMeasured,
179
+ isTouchable,
180
+ mounted,
181
+ onLayout,
182
+ opacity,
183
+ translateY
184
+ };
185
+ }
186
+ export {
187
+ useFlyoutLifecycle
188
+ };
189
+ //# sourceMappingURL=use-flyout-lifecycle.hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/use-flyout-lifecycle.hook.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { LayoutChangeEvent } from \"react-native\";\nimport { Gesture } from \"react-native-gesture-handler\";\nimport {\n Easing,\n useSharedValue,\n withSpring,\n withTiming,\n} from \"react-native-reanimated\";\nimport { scheduleOnRN } from \"react-native-worklets\";\n\nimport type { FlyoutProps } from \"../types\";\nimport { FlyoutHandleHeight } from \"../constants\";\n\nconst EnterAnimationConfig = {\n damping: 50,\n stiffness: 300,\n mass: 1,\n overshootClamping: true,\n};\n\nconst ExitAnimationConfig = {\n duration: 150,\n easing: Easing.in(Easing.quad),\n};\n\ninterface UseFlyoutLifecycleProps {\n id: FlyoutProps[\"id\"];\n open: FlyoutProps[\"open\"];\n placement: NonNullable<FlyoutProps[\"placement\"]>;\n screenHeight: number;\n safeAreaInset: number;\n overlayVisibleOpacity: number;\n onClose: FlyoutProps[\"onClose\"];\n onCloseFinished: FlyoutProps[\"onCloseFinished\"];\n onOpen: FlyoutProps[\"onOpen\"];\n onOpenFinished: FlyoutProps[\"onOpenFinished\"];\n}\n\nexport function useFlyoutLifecycle(props: UseFlyoutLifecycleProps) {\n const {\n id,\n open,\n placement,\n screenHeight,\n safeAreaInset,\n overlayVisibleOpacity,\n onClose,\n onCloseFinished,\n onOpen,\n onOpenFinished,\n } = props;\n\n const [mounted, setMounted] = useState(open);\n const [measuredHeight, setMeasuredHeight] = useState<number | null>(null);\n const [isTouchable, setIsTouchable] = useState(false);\n\n const didOpen = useRef(false);\n const isClosing = useRef(false);\n const transitionId = useRef(0);\n\n const translateY = useSharedValue(\n placement === \"top\" ? -screenHeight : screenHeight,\n );\n const opacity = useSharedValue(0);\n\n const hasMeasured = measuredHeight !== null;\n\n const onLayout = useCallback(\n (event: LayoutChangeEvent) => {\n if (measuredHeight !== null) {\n return;\n }\n\n const height =\n event.nativeEvent.layout.height + safeAreaInset + FlyoutHandleHeight;\n\n translateY.value = placement === \"top\" ? -height : height;\n setMeasuredHeight(height);\n },\n [measuredHeight, placement, safeAreaInset, translateY],\n );\n\n const finishClosing = useCallback(\n (finishedTransitionId: number) => {\n if (!isClosing.current || transitionId.current !== finishedTransitionId) {\n return;\n }\n\n isClosing.current = false;\n setMounted(false);\n setMeasuredHeight(null);\n onCloseFinished?.(id);\n },\n [id, onCloseFinished],\n );\n\n const close = useCallback(() => {\n if (measuredHeight === null || isClosing.current) {\n return;\n }\n\n isClosing.current = true;\n transitionId.current += 1;\n setIsTouchable(false);\n onClose?.(id);\n\n const target = placement === \"top\" ? -measuredHeight : measuredHeight;\n const closingTransitionId = transitionId.current;\n\n translateY.value = withTiming(target, ExitAnimationConfig, (finished) => {\n if (finished) {\n scheduleOnRN(finishClosing, closingTransitionId);\n }\n });\n opacity.value = withTiming(0, ExitAnimationConfig);\n didOpen.current = false;\n }, [\n finishClosing,\n id,\n measuredHeight,\n onClose,\n opacity,\n placement,\n translateY,\n ]);\n\n const dragGesture = useMemo(\n () =>\n Gesture.Pan()\n .enabled(hasMeasured)\n .onUpdate((event) => {\n if (placement === \"bottom\") {\n translateY.value = Math.max(0, event.translationY);\n return;\n }\n\n translateY.value = Math.min(0, event.translationY);\n })\n .onEnd((event) => {\n if (measuredHeight === null) {\n return;\n }\n\n const shouldClose =\n placement === \"bottom\"\n ? event.translationY > measuredHeight * 0.5 ||\n event.velocityY > 1500\n : event.translationY < -measuredHeight * 0.5 ||\n event.velocityY < -1500;\n\n if (shouldClose) {\n scheduleOnRN(close);\n return;\n }\n\n translateY.value = withSpring(0, EnterAnimationConfig);\n opacity.value = withSpring(\n overlayVisibleOpacity,\n EnterAnimationConfig,\n );\n }),\n [\n close,\n hasMeasured,\n measuredHeight,\n opacity,\n overlayVisibleOpacity,\n placement,\n translateY,\n ],\n );\n\n useEffect(() => {\n if (!open) {\n return;\n }\n\n transitionId.current += 1;\n setIsTouchable(false);\n isClosing.current = false;\n didOpen.current = false;\n\n if (!mounted) {\n setMounted(true);\n setMeasuredHeight(null);\n translateY.value = placement === \"top\" ? -screenHeight : screenHeight;\n opacity.value = 0;\n }\n }, [mounted, open, opacity, placement, screenHeight, translateY]);\n\n useEffect(() => {\n if (!open || measuredHeight === null || didOpen.current) {\n return;\n }\n\n setIsTouchable(true);\n onOpen?.(id);\n\n translateY.value = withSpring(0, EnterAnimationConfig, (finished) => {\n if (finished && onOpenFinished) {\n scheduleOnRN(onOpenFinished, id);\n }\n });\n opacity.value = withSpring(overlayVisibleOpacity, EnterAnimationConfig);\n didOpen.current = true;\n }, [\n id,\n measuredHeight,\n onOpen,\n onOpenFinished,\n opacity,\n open,\n overlayVisibleOpacity,\n translateY,\n ]);\n\n useEffect(() => {\n if (open || !mounted || isClosing.current) {\n return;\n }\n\n if (didOpen.current) {\n close();\n return;\n }\n\n setMounted(false);\n setMeasuredHeight(null);\n }, [close, mounted, open]);\n\n return {\n close,\n dragGesture,\n hasMeasured,\n isTouchable,\n mounted,\n onLayout,\n opacity,\n translateY,\n };\n}\n"],"mappings":"AAAA,SAAS,aAAa,WAAW,SAAS,QAAQ,gBAAgB;AAElE,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAG7B,SAAS,0BAA0B;AAEnC,MAAM,uBAAuB;AAAA,EAC3B,SAAS;AAAA,EACT,WAAW;AAAA,EACX,MAAM;AAAA,EACN,mBAAmB;AACrB;AAEA,MAAM,sBAAsB;AAAA,EAC1B,UAAU;AAAA,EACV,QAAQ,OAAO,GAAG,OAAO,IAAI;AAC/B;AAeO,SAAS,mBAAmB,OAAgC;AACjE,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAwB,IAAI;AACxE,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,UAAU,OAAO,KAAK;AAC5B,QAAM,YAAY,OAAO,KAAK;AAC9B,QAAM,eAAe,OAAO,CAAC;AAE7B,QAAM,aAAa;AAAA,IACjB,cAAc,QAAQ,CAAC,eAAe;AAAA,EACxC;AACA,QAAM,UAAU,eAAe,CAAC;AAEhC,QAAM,cAAc,mBAAmB;AAEvC,QAAM,WAAW;AAAA,IACf,CAAC,UAA6B;AAC5B,UAAI,mBAAmB,MAAM;AAC3B;AAAA,MACF;AAEA,YAAM,SACJ,MAAM,YAAY,OAAO,SAAS,gBAAgB;AAEpD,iBAAW,QAAQ,cAAc,QAAQ,CAAC,SAAS;AACnD,wBAAkB,MAAM;AAAA,IAC1B;AAAA,IACA,CAAC,gBAAgB,WAAW,eAAe,UAAU;AAAA,EACvD;AAEA,QAAM,gBAAgB;AAAA,IACpB,CAAC,yBAAiC;AAChC,UAAI,CAAC,UAAU,WAAW,aAAa,YAAY,sBAAsB;AACvE;AAAA,MACF;AAEA,gBAAU,UAAU;AACpB,iBAAW,KAAK;AAChB,wBAAkB,IAAI;AACtB,wBAAkB,EAAE;AAAA,IACtB;AAAA,IACA,CAAC,IAAI,eAAe;AAAA,EACtB;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,QAAI,mBAAmB,QAAQ,UAAU,SAAS;AAChD;AAAA,IACF;AAEA,cAAU,UAAU;AACpB,iBAAa,WAAW;AACxB,mBAAe,KAAK;AACpB,cAAU,EAAE;AAEZ,UAAM,SAAS,cAAc,QAAQ,CAAC,iBAAiB;AACvD,UAAM,sBAAsB,aAAa;AAEzC,eAAW,QAAQ,WAAW,QAAQ,qBAAqB,CAAC,aAAa;AACvE,UAAI,UAAU;AACZ,qBAAa,eAAe,mBAAmB;AAAA,MACjD;AAAA,IACF,CAAC;AACD,YAAQ,QAAQ,WAAW,GAAG,mBAAmB;AACjD,YAAQ,UAAU;AAAA,EACpB,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,cAAc;AAAA,IAClB,MACE,QAAQ,IAAI,EACT,QAAQ,WAAW,EACnB,SAAS,CAAC,UAAU;AACnB,UAAI,cAAc,UAAU;AAC1B,mBAAW,QAAQ,KAAK,IAAI,GAAG,MAAM,YAAY;AACjD;AAAA,MACF;AAEA,iBAAW,QAAQ,KAAK,IAAI,GAAG,MAAM,YAAY;AAAA,IACnD,CAAC,EACA,MAAM,CAAC,UAAU;AAChB,UAAI,mBAAmB,MAAM;AAC3B;AAAA,MACF;AAEA,YAAM,cACJ,cAAc,WACV,MAAM,eAAe,iBAAiB,OACtC,MAAM,YAAY,OAClB,MAAM,eAAe,CAAC,iBAAiB,OACvC,MAAM,YAAY;AAExB,UAAI,aAAa;AACf,qBAAa,KAAK;AAClB;AAAA,MACF;AAEA,iBAAW,QAAQ,WAAW,GAAG,oBAAoB;AACrD,cAAQ,QAAQ;AAAA,QACd;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAAA,IACL;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,YAAU,MAAM;AACd,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,iBAAa,WAAW;AACxB,mBAAe,KAAK;AACpB,cAAU,UAAU;AACpB,YAAQ,UAAU;AAElB,QAAI,CAAC,SAAS;AACZ,iBAAW,IAAI;AACf,wBAAkB,IAAI;AACtB,iBAAW,QAAQ,cAAc,QAAQ,CAAC,eAAe;AACzD,cAAQ,QAAQ;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,SAAS,MAAM,SAAS,WAAW,cAAc,UAAU,CAAC;AAEhE,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,mBAAmB,QAAQ,QAAQ,SAAS;AACvD;AAAA,IACF;AAEA,mBAAe,IAAI;AACnB,aAAS,EAAE;AAEX,eAAW,QAAQ,WAAW,GAAG,sBAAsB,CAAC,aAAa;AACnE,UAAI,YAAY,gBAAgB;AAC9B,qBAAa,gBAAgB,EAAE;AAAA,MACjC;AAAA,IACF,CAAC;AACD,YAAQ,QAAQ,WAAW,uBAAuB,oBAAoB;AACtE,YAAQ,UAAU;AAAA,EACpB,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,YAAU,MAAM;AACd,QAAI,QAAQ,CAAC,WAAW,UAAU,SAAS;AACzC;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS;AACnB,YAAM;AACN;AAAA,IACF;AAEA,eAAW,KAAK;AAChB,sBAAkB,IAAI;AAAA,EACxB,GAAG,CAAC,OAAO,SAAS,IAAI,CAAC;AAEzB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
package/dist/index.d.mts CHANGED
@@ -2,4 +2,4 @@ export { Flyout } from './components/flyout.mjs';
2
2
  export { FlyoutProps } from './types/flyout.types.mjs';
3
3
  import 'react';
4
4
  import 'react/jsx-runtime';
5
- import '@impulse-ui-native/types';
5
+ import '@impulse-ui-native/overlay';
@@ -1,7 +1,6 @@
1
- import { LayerComponentBaseProps } from '@impulse-ui-native/types';
1
+ import { OverlayComponentProps } from '@impulse-ui-native/overlay';
2
2
 
3
- interface FlyoutProps extends LayerComponentBaseProps {
4
- open?: boolean;
3
+ interface FlyoutProps extends OverlayComponentProps {
5
4
  placement?: "top" | "bottom";
6
5
  topOffset?: number;
7
6
  bottomOffset?: number;
@@ -1,2 +1,2 @@
1
1
  export { FlyoutProps } from './flyout.types.mjs';
2
- import '@impulse-ui-native/types';
2
+ import '@impulse-ui-native/overlay';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulse-ui-native/flyout",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "description": "Flyout, popover, and overlay components for ImpulseUI Native applications.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -28,10 +28,9 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@impulse-ui-native/primitives": "2.3.5",
32
- "@impulse-ui-native/core": "2.3.5",
33
- "@impulse-ui-native/theme": "2.3.5",
34
- "@impulse-ui-native/types": "2.3.5"
31
+ "@impulse-ui-native/overlay": "2.4.0",
32
+ "@impulse-ui-native/primitives": "2.4.0",
33
+ "@impulse-ui-native/theme": "2.4.0"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@types/react": "~19.2.14",
@@ -44,9 +43,9 @@
44
43
  "react-native-safe-area-context": "~5.7.0",
45
44
  "react-native-worklets": "0.10.1",
46
45
  "typescript": "~6.0.3",
47
- "@impulse-ui-native/prettier-config": "1.0.0",
48
46
  "@impulse-ui-native/eslint-config": "1.0.0",
49
- "@impulse-ui-native/tsconfig": "1.0.0"
47
+ "@impulse-ui-native/tsconfig": "1.0.0",
48
+ "@impulse-ui-native/prettier-config": "1.0.0"
50
49
  },
51
50
  "peerDependencies": {
52
51
  "react": ">=18",