@hoddy-ui/core 1.1.3 → 2.0.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
@@ -27,15 +27,17 @@ yarn add @hoddy-ui/core
27
27
  Install the required peer dependencies:
28
28
 
29
29
  ```bash
30
- npm install @expo/vector-icons @react-native-async-storage/async-storage @react-navigation/native expo-navigation-bar expo-system-ui react-native-safe-area-context react-native-size-matters
30
+ npm install @expo/vector-icons @react-native-async-storage/async-storage @react-navigation/native expo-navigation-bar expo-system-ui react-native-safe-area-context react-native-size-matters react-native-reanimated
31
31
  ```
32
32
 
33
33
  Or with yarn:
34
34
 
35
35
  ```bash
36
- yarn add @expo/vector-icons @react-native-async-storage/async-storage @react-navigation/native expo-navigation-bar expo-system-ui react-native-safe-area-context react-native-size-matters
36
+ yarn add @expo/vector-icons @react-native-async-storage/async-storage @react-navigation/native expo-navigation-bar expo-system-ui react-native-safe-area-context react-native-size-matters react-native-reanimated
37
37
  ```
38
38
 
39
+ **Important**: Make sure to follow the [react-native-reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/3.x/fundamentals/getting-started) for platform-specific setup as it requires additional configuration for iOS and Android.
40
+
39
41
  ## 🚀 Quick Start
40
42
 
41
43
  ### Basic Setup
@@ -257,6 +259,7 @@ const colors = {
257
259
 
258
260
  - **`TextField`** - Material Design text input with variants
259
261
  - **`TextField2`** - Alternative text field design
262
+ - **`OTPInput`** - One-Time Password input with auto-advance and paste support
260
263
  - **`Locator`** - Location picker with Google Maps integration
261
264
 
262
265
  ### Interactive Elements
@@ -433,6 +436,57 @@ A Material Design text input component with comprehensive features.
433
436
  />
434
437
  ```
435
438
 
439
+ ### OTPInput
440
+
441
+ A specialized input component for One-Time Password entry with auto-advance, paste support, and backspace handling.
442
+
443
+ **Props:**
444
+
445
+ | Prop | Type | Default | Description |
446
+ | ---------- | ------------------------------------- | ------------ | -------------------------------- |
447
+ | `length` | `number` | `6` | Number of OTP digits |
448
+ | `onChange` | `(value: string) => void` | - | Change handler for OTP value |
449
+ | `value` | `string` | `""` | Current OTP value |
450
+ | `variant` | `"outlined" \| "text" \| "contained"` | `"outlined"` | Input variant style |
451
+ | `spacing` | `number` | `1` | Spacing between input boxes |
452
+ | `size` | `number` | `45` | Size of each input box in pixels |
453
+
454
+ **Features:**
455
+
456
+ - **Auto-advance**: Automatically moves to next input after digit entry
457
+ - **Paste support**: Handles pasting of complete OTP codes
458
+ - **Backspace handling**: Moves to previous input on backspace
459
+ - **Number-only input**: Automatically filters non-numeric characters
460
+ - **Customizable styling**: Supports different variants and sizes
461
+
462
+ **Example:**
463
+
464
+ ```tsx
465
+ const [otp, setOtp] = useState('');
466
+
467
+ <OTPInput
468
+ length={6}
469
+ value={otp}
470
+ onChange={setOtp}
471
+ variant="outlined"
472
+ size={50}
473
+ spacing={2}
474
+ />
475
+
476
+ // Usage with form validation
477
+ <OTPInput
478
+ length={4}
479
+ value={otp}
480
+ onChange={(value) => {
481
+ setOtp(value);
482
+ if (value.length === 4) {
483
+ verifyOTP(value);
484
+ }
485
+ }}
486
+ variant="contained"
487
+ />
488
+ ```
489
+
436
490
  ### Locator
437
491
 
438
492
  A location picker component with Google Maps integration.
@@ -674,6 +728,8 @@ A modal component for overlays, dialogs, and bottom sheets.
674
728
  | `sheet` | `boolean \| number` | `false` | Bottom sheet mode/height |
675
729
  | `bare` | `boolean` | `false` | Hide header and padding |
676
730
  | `keyboardVerticalOffset` | `number` | - | Keyboard avoidance offset |
731
+ | `onModalShow` | `() => void` | - | Callback when modal shows |
732
+ | `onModalHide` | `() => void` | - | Callback when modal hides |
677
733
  | `style` | `ViewStyle` | `{}` | Container style overrides |
678
734
 
679
735
  **Example:**
@@ -684,6 +740,8 @@ A modal component for overlays, dialogs, and bottom sheets.
684
740
  onClose={() => setIsOpen(false)}
685
741
  title="Confirm Action"
686
742
  sheet={400}
743
+ onModalShow={() => console.log("Modal is now visible")}
744
+ onModalHide={() => console.log("Modal is now hidden")}
687
745
  >
688
746
  <Typography>Are you sure you want to delete this item?</Typography>
689
747
  <Button title="Delete" color="error" />
@@ -713,7 +771,7 @@ A loading indicator component with customizable appearance.
713
771
 
714
772
  ### Animator
715
773
 
716
- A unified component that provides a single interface for all animation types with generic props.
774
+ A unified component that provides a single interface for all animation types with generic props. Built with **react-native-reanimated** for optimal performance and smooth animations.
717
775
 
718
776
  **Features:**
719
777
 
@@ -721,7 +779,12 @@ A unified component that provides a single interface for all animation types wit
721
779
  - **Generic Props**: Consistent prop naming across all animations (e.g., `closeAfter` instead of animation-specific names)
722
780
  - **Modular Hooks**: Animation logic is separated into individual custom hooks
723
781
  - **Type Safety**: Full TypeScript support with proper typing
724
- - **Performance**: Only the specified animation runs, others are not loaded
782
+ - **Performance**: Built with react-native-reanimated for native performance
783
+ - **Smooth Animations**: Runs on the UI thread for 60fps animations
784
+
785
+ **Requirements:**
786
+
787
+ Requires `react-native-reanimated` as peer dependencies. Make sure to follow the [react-native-reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/3.x/fundamentals/getting-started) for platform-specific setup.
725
788
 
726
789
  **Generic Props:**
727
790
 
@@ -804,20 +867,23 @@ All animation types support these generic props:
804
867
 
805
868
  **Available Animation Types:**
806
869
 
807
- 1. **fade**: Simple fade in/out
808
- 2. **grow**: Scale-based growth animation
809
- 3. **slide**: Directional slide animations
810
- 4. **blink**: Continuous opacity blinking
811
- 5. **float**: Floating up/down motion with fade
812
- 6. **roll**: Combined rotation and translation
813
- 7. **thrownup**: Spring-based upward animation
870
+ 1. **fade**: Simple fade in/out using opacity (native performance)
871
+ 2. **grow**: Scale-based growth animation with easing
872
+ 3. **slide**: Directional slide animations from screen edges
873
+ 4. **blink**: Continuous opacity blinking with repeat
874
+ 5. **float**: Floating up/down motion with fade effects
875
+ 6. **roll**: Combined rotation and translation effects
876
+ 7. **thrownup**: Spring-based upward animation with physics
877
+
878
+ All animations run on the UI thread for optimal performance and smooth 60fps animations.
814
879
 
815
880
  **Using Animation Hooks Directly:**
816
881
 
817
- You can also use the animation hooks directly for custom implementations:
882
+ You can also use the animation hooks directly for custom implementations with react-native-reanimated:
818
883
 
819
884
  ```tsx
820
885
  import { useFadeAnimation, useSlideAnimation } from "@hoddy-ui/core";
886
+ import Animated from "react-native-reanimated";
821
887
 
822
888
  const MyComponent = () => {
823
889
  const { animatedStyle } = useFadeAnimation({
@@ -833,7 +899,17 @@ const MyComponent = () => {
833
899
  };
834
900
  ```
835
901
 
836
- **Migration from Old Components:**
902
+ **Performance Benefits:**
903
+
904
+ With react-native-reanimated, all animations:
905
+
906
+ - **Run on the UI thread** for better performance
907
+ - **Maintain 60fps** even during JavaScript thread blocking
908
+ - **Use native drivers** automatically
909
+ - **Support worklets** for complex animation logic
910
+ - **Provide smooth gestures** and interactions
911
+
912
+ **Migration from Legacy API:**
837
913
 
838
914
  Replace old individual animation components:
839
915
 
@@ -933,12 +1009,13 @@ function MyScreen() {
933
1009
 
934
1010
  ### Animation Hooks
935
1011
 
936
- Access animation logic directly for custom implementations:
1012
+ Access animation logic directly for custom implementations using react-native-reanimated:
937
1013
 
938
1014
  #### useFadeAnimation
939
1015
 
940
1016
  ```tsx
941
1017
  import { useFadeAnimation } from "@hoddy-ui/core";
1018
+ import Animated from "react-native-reanimated";
942
1019
 
943
1020
  function FadeComponent() {
944
1021
  const { animatedStyle } = useFadeAnimation({
@@ -958,6 +1035,7 @@ function FadeComponent() {
958
1035
 
959
1036
  ```tsx
960
1037
  import { useSlideAnimation } from "@hoddy-ui/core";
1038
+ import Animated from "react-native-reanimated";
961
1039
 
962
1040
  function SlideComponent() {
963
1041
  const { animatedStyle } = useSlideAnimation({
@@ -978,6 +1056,7 @@ function SlideComponent() {
978
1056
 
979
1057
  ```tsx
980
1058
  import { useGrowAnimation } from "@hoddy-ui/core";
1059
+ import Animated from "react-native-reanimated";
981
1060
 
982
1061
  function GrowComponent() {
983
1062
  const { animatedStyle } = useGrowAnimation({
@@ -1000,7 +1079,7 @@ function GrowComponent() {
1000
1079
  - `useRollAnimation` - For rotation and translation effects
1001
1080
  - `useThrownUpAnimation` - For spring-based upward animations
1002
1081
 
1003
- All animation hooks accept similar configuration objects with properties like `duration`, `delay`, and animation-specific options.
1082
+ All animation hooks accept similar configuration objects with properties like `duration`, `delay`, and animation-specific options. They return `animatedStyle` objects that work with `Animated.View` from react-native-reanimated for optimal performance.
1004
1083
 
1005
1084
  ## 🎯 Advanced Usage
1006
1085
 
@@ -1110,4 +1189,4 @@ MIT © [Hoddy Inc](https://github.com/kinghoddy)
1110
1189
 
1111
1190
  ---
1112
1191
 
1113
- **Need help?** [Open an issue](https://github.com/kinghoddy/hoddy-ui/issues) or check out our [examples](../../test/hoddyui).
1192
+ **Need help?** [Open an issue](https://github.com/kinghoddy/hoddy-ui/issues) or check out our [examples](../../test/my-app).
package/next/package.json CHANGED
@@ -19,17 +19,17 @@
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@expo/vector-icons": ">=13.0.0",
22
- "@types/react": "^18.2.6",
22
+ "@types/react": ">=18.2.6",
23
23
  "@types/react-native": "^0.72.0",
24
24
  "expo-haptics": ">=12.4.0",
25
25
  "expo-location": ">=15.1.1",
26
26
  "expo-navigation-bar": ">=2.1.1",
27
27
  "expo-router": ">=1.0.0",
28
28
  "expo-system-ui": ">=2.2.1",
29
- "react": "^18.2.0",
29
+ "react": ">=18.2.0",
30
30
  "react-native": ">=0.71.8",
31
31
  "react-native-safe-area-context": ">=4.5.3",
32
- "typescript": "^5.0.4"
32
+ "typescript": ">=5.0.4"
33
33
  },
34
34
  "keywords": [
35
35
  "react-native",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoddy-ui/core",
3
- "version": "1.1.3",
3
+ "version": "2.0.0",
4
4
  "description": "Core rich react native components written in typescript",
5
5
  "main": "index.ts",
6
6
  "repository": {
@@ -13,7 +13,7 @@
13
13
  "private": false,
14
14
  "peerDependencies": {
15
15
  "@expo/vector-icons": ">=13.0.0",
16
- "@react-navigation/native": ">=6.1.7",
16
+ "@react-navigation/native": ">=6.1.6",
17
17
  "@types/react": ">=18.2.6",
18
18
  "@types/react-native": ">=0.72.0",
19
19
  "expo-haptics": ">=12.4.0",
@@ -22,6 +22,7 @@
22
22
  "expo-system-ui": ">=2.2.1",
23
23
  "react": ">=18.2.0",
24
24
  "react-native": ">=0.71.8",
25
+ "react-native-reanimated": ">=3.17.4",
25
26
  "react-native-safe-area-context": ">=4.5.3",
26
27
  "typescript": ">=5.0.4"
27
28
  },
@@ -34,9 +35,10 @@
34
35
  ],
35
36
  "dependencies": {
36
37
  "@react-native-async-storage/async-storage": "^1.18.1",
37
- "react-native-size-matters": "^0.4.0"
38
+ "react-native-size-matters": "^0.4.2"
38
39
  },
39
40
  "publishConfig": {
40
41
  "access": "public"
41
- }
42
+ },
43
+ "devDependencies": {}
42
44
  }
@@ -1,5 +1,5 @@
1
1
  import React, { FC } from "react";
2
- import { Animated } from "react-native";
2
+ import Animated from "react-native-reanimated";
3
3
  import { AnimatorProps } from "../../types";
4
4
  import { useBlinkAnimation } from "./hooks/useBlinkAnimation";
5
5
  import { useFadeAnimation } from "./hooks/useFadeAnimation";
@@ -46,7 +46,7 @@ import { useThrownUpAnimation } from "./hooks/useThrownUpAnimation";
46
46
  * // ✅ This is correct:
47
47
  * // <Animator type="slide" direction="up">
48
48
  */
49
- const Animator: FC<AnimatorProps> = (props) => {
49
+ export const Animator: FC<AnimatorProps> = (props) => {
50
50
  const { children, type, duration, delay, closeAfter, style = {} } = props;
51
51
 
52
52
  // Get animation style based on type
@@ -113,5 +113,3 @@ const Animator: FC<AnimatorProps> = (props) => {
113
113
  <Animated.View style={[style, animatedStyle]}>{children}</Animated.View>
114
114
  );
115
115
  };
116
-
117
- export default Animator;
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
2
2
  import { AppState, Platform } from "react-native";
3
3
 
4
4
  const useAppState = () => {
5
- const [isActive, setIsActive] = useState(true);
5
+ const [isActive, setIsActive] = useState(AppState.currentState === "active");
6
6
 
7
7
  useEffect(() => {
8
8
  const handleAppStateChange = (nextAppState: string) => {
@@ -10,24 +10,17 @@ const useAppState = () => {
10
10
  };
11
11
 
12
12
  let subscription: any;
13
- let blurSub: any;
14
- let focusSub: any;
15
13
 
16
14
  if (Platform.OS === "android") {
17
- blurSub = AppState.addEventListener("blur", () =>
18
- handleAppStateChange("inactive")
19
- );
20
- focusSub = AppState.addEventListener("focus", () =>
21
- handleAppStateChange("active")
22
- );
15
+ // For Android, use the change event which covers both blur and focus
16
+ subscription = AppState.addEventListener("change", handleAppStateChange);
23
17
  } else {
18
+ // For iOS, use the change event as well
24
19
  subscription = AppState.addEventListener("change", handleAppStateChange);
25
20
  }
26
21
 
27
22
  return () => {
28
23
  subscription?.remove();
29
- blurSub?.remove();
30
- focusSub?.remove();
31
24
  };
32
25
  }, []);
33
26
 
@@ -1,5 +1,13 @@
1
- import { useEffect, useRef } from "react";
2
- import { Animated, Easing, Platform } from "react-native";
1
+ import { useEffect } from "react";
2
+ import { Platform } from "react-native";
3
+ import {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withRepeat,
8
+ withSequence,
9
+ withTiming,
10
+ } from "react-native-reanimated";
3
11
  import useAppState from "./useAppState";
4
12
 
5
13
  interface UseBlinkAnimationProps {
@@ -15,57 +23,56 @@ export const useBlinkAnimation = ({
15
23
  minOpacity = 0.5,
16
24
  maxOpacity = 1,
17
25
  }: UseBlinkAnimationProps = {}) => {
18
- const opacity = useRef(new Animated.Value(maxOpacity)).current;
26
+ const opacity = useSharedValue(maxOpacity);
19
27
  const { isActive } = useAppState();
20
- const blinkAnim = useRef<Animated.CompositeAnimation | null>(null);
28
+
29
+ const animatedStyle = useAnimatedStyle(() => {
30
+ return {
31
+ opacity: opacity.value,
32
+ };
33
+ });
21
34
 
22
35
  const startBlinking = () => {
23
- blinkAnim.current = Animated.loop(
24
- Animated.sequence([
25
- Animated.timing(opacity, {
26
- toValue: minOpacity,
36
+ opacity.value = withRepeat(
37
+ withSequence(
38
+ withTiming(minOpacity, {
27
39
  duration: blinkDuration / 2,
28
40
  easing: Easing.inOut(Easing.quad),
29
- useNativeDriver: true,
30
41
  }),
31
- Animated.timing(opacity, {
32
- toValue: maxOpacity,
42
+ withTiming(maxOpacity, {
33
43
  duration: blinkDuration / 2,
34
44
  easing: Easing.inOut(Easing.quad),
35
- useNativeDriver: true,
36
- }),
37
- ])
45
+ })
46
+ ),
47
+ -1,
48
+ false
38
49
  );
39
- blinkAnim.current.start();
40
50
  };
41
51
 
42
52
  useEffect(() => {
43
53
  if (!isActive && Platform.OS === "ios") {
44
- opacity.stopAnimation();
54
+ opacity.value = maxOpacity;
55
+ return;
45
56
  }
46
- }, [isActive]);
47
57
 
48
- useEffect(() => {
49
58
  if (delay > 0) {
50
59
  const timer = setTimeout(() => {
51
60
  startBlinking();
52
61
  }, delay);
53
62
  return () => {
54
63
  clearTimeout(timer);
55
- opacity.stopAnimation();
56
- blinkAnim.current?.stop();
64
+ opacity.value = maxOpacity;
57
65
  };
58
66
  } else {
59
67
  startBlinking();
60
68
  }
61
69
 
62
70
  return () => {
63
- opacity.stopAnimation();
64
- blinkAnim.current?.stop();
71
+ opacity.value = maxOpacity;
65
72
  };
66
- }, [delay, blinkDuration, minOpacity, maxOpacity]);
73
+ }, [delay, blinkDuration, minOpacity, maxOpacity, isActive]);
67
74
 
68
75
  return {
69
- animatedStyle: { opacity },
76
+ animatedStyle,
70
77
  };
71
78
  };
@@ -1,5 +1,11 @@
1
- import { useEffect, useRef } from "react";
2
- import { Animated, Platform } from "react-native";
1
+ import { useEffect } from "react";
2
+ import { Platform } from "react-native";
3
+ import {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withDelay,
7
+ withTiming,
8
+ } from "react-native-reanimated";
3
9
  import useAppState from "./useAppState";
4
10
 
5
11
  interface UseFadeAnimationProps {
@@ -11,40 +17,38 @@ interface UseFadeAnimationProps {
11
17
  export const useFadeAnimation = ({
12
18
  duration = 1000,
13
19
  delay = 0,
14
- closeAfter = 2000,
20
+ closeAfter = null,
15
21
  }: UseFadeAnimationProps = {}) => {
16
- const opacity = useRef(new Animated.Value(0)).current;
22
+ const opacity = useSharedValue(0);
17
23
  const { isActive } = useAppState();
18
24
 
25
+ const animatedStyle = useAnimatedStyle(() => {
26
+ return {
27
+ opacity: opacity.value,
28
+ };
29
+ });
30
+
19
31
  useEffect(() => {
20
32
  if (!isActive && Platform.OS === "ios") {
21
- opacity.stopAnimation();
33
+ opacity.value = 0;
34
+ return;
22
35
  }
23
- }, [isActive]);
24
36
 
25
- useEffect(() => {
26
37
  // Fade-in animation
27
- Animated.timing(opacity, {
28
- toValue: 1,
29
- duration,
38
+ opacity.value = withDelay(
30
39
  delay,
31
- useNativeDriver: true,
32
- }).start(() => {
33
- if (closeAfter) {
34
- setTimeout(() => {
35
- Animated.timing(opacity, {
36
- toValue: 0,
37
- duration,
38
- useNativeDriver: true,
39
- }).start();
40
- }, closeAfter);
41
- }
42
- });
43
-
44
- return () => opacity.stopAnimation();
45
- }, [opacity, duration, delay, closeAfter]);
40
+ withTiming(1, { duration }, () => {
41
+ if (closeAfter) {
42
+ // Schedule fade-out after closeAfter duration
43
+ setTimeout(() => {
44
+ opacity.value = withTiming(0, { duration });
45
+ }, closeAfter);
46
+ }
47
+ })
48
+ );
49
+ }, [opacity, duration, delay, closeAfter, isActive]);
46
50
 
47
51
  return {
48
- animatedStyle: { opacity },
52
+ animatedStyle,
49
53
  };
50
54
  };
@@ -1,5 +1,14 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { Animated, Easing, Platform } from "react-native";
2
+ import { Platform } from "react-native";
3
+ import {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withDelay,
8
+ withRepeat,
9
+ withSequence,
10
+ withTiming,
11
+ } from "react-native-reanimated";
3
12
  import useAppState from "./useAppState";
4
13
 
5
14
  interface UseFloatAnimationProps {
@@ -14,76 +23,79 @@ interface UseFloatAnimationProps {
14
23
  export const useFloatAnimation = ({
15
24
  duration = 800,
16
25
  delay = 0,
17
- closeAfter = 2000,
26
+ closeAfter = null,
18
27
  closeDuration = 600,
19
28
  floatDistance = 10,
20
29
  floatDuration = 1200,
21
30
  }: UseFloatAnimationProps = {}) => {
22
- const opacity = useRef(new Animated.Value(0)).current;
23
- const translateY = useRef(new Animated.Value(0)).current;
31
+ const opacity = useSharedValue(0);
32
+ const translateY = useSharedValue(0);
24
33
  const { isActive } = useAppState();
25
- const floatAnim = useRef<Animated.CompositeAnimation | null>(null);
34
+ const isFloating = useRef(false);
35
+
36
+ const animatedStyle = useAnimatedStyle(() => {
37
+ return {
38
+ opacity: opacity.value,
39
+ transform: [{ translateY: translateY.value }],
40
+ };
41
+ });
26
42
 
27
43
  const startFloating = () => {
28
- floatAnim.current = Animated.loop(
29
- Animated.sequence([
30
- Animated.timing(translateY, {
31
- toValue: -floatDistance,
32
- duration: floatDuration / 2,
33
- easing: Easing.inOut(Easing.quad),
34
- useNativeDriver: true,
35
- }),
36
- Animated.timing(translateY, {
37
- toValue: floatDistance,
38
- duration: floatDuration,
39
- easing: Easing.inOut(Easing.quad),
40
- useNativeDriver: true,
41
- }),
42
- Animated.timing(translateY, {
43
- toValue: 0,
44
- duration: floatDuration / 2,
45
- easing: Easing.inOut(Easing.quad),
46
- useNativeDriver: true,
47
- }),
48
- ])
49
- );
50
- floatAnim.current.start();
44
+ if (!isFloating.current) {
45
+ isFloating.current = true;
46
+ translateY.value = withRepeat(
47
+ withSequence(
48
+ withTiming(-floatDistance, {
49
+ duration: floatDuration / 2,
50
+ easing: Easing.inOut(Easing.quad),
51
+ }),
52
+ withTiming(floatDistance, {
53
+ duration: floatDuration,
54
+ easing: Easing.inOut(Easing.quad),
55
+ }),
56
+ withTiming(0, {
57
+ duration: floatDuration / 2,
58
+ easing: Easing.inOut(Easing.quad),
59
+ })
60
+ ),
61
+ -1,
62
+ false
63
+ );
64
+ }
65
+ };
66
+
67
+ const stopFloating = () => {
68
+ isFloating.current = false;
69
+ translateY.value = withTiming(0, { duration: 200 });
51
70
  };
52
71
 
53
72
  useEffect(() => {
54
73
  if (!isActive && Platform.OS === "ios") {
55
- opacity.stopAnimation();
56
- translateY.stopAnimation();
74
+ opacity.value = 0;
75
+ translateY.value = 0;
76
+ isFloating.current = false;
77
+ return;
57
78
  }
58
- }, [isActive]);
59
79
 
60
- useEffect(() => {
61
80
  // Fade-in
62
- Animated.timing(opacity, {
63
- toValue: 1,
64
- duration,
81
+ opacity.value = withDelay(
65
82
  delay,
66
- useNativeDriver: true,
67
- }).start(() => {
68
- startFloating();
83
+ withTiming(1, { duration }, () => {
84
+ startFloating();
69
85
 
70
- if (closeAfter) {
71
- setTimeout(() => {
72
- floatAnim.current?.stop();
73
-
74
- Animated.timing(opacity, {
75
- toValue: 0,
76
- duration: closeDuration,
77
- useNativeDriver: true,
78
- }).start();
79
- }, closeAfter);
80
- }
81
- });
86
+ if (closeAfter) {
87
+ setTimeout(() => {
88
+ stopFloating();
89
+ opacity.value = withTiming(0, { duration: closeDuration });
90
+ }, closeAfter);
91
+ }
92
+ })
93
+ );
82
94
 
83
95
  return () => {
84
- opacity.stopAnimation();
85
- translateY.stopAnimation();
86
- floatAnim.current?.stop();
96
+ opacity.value = 0;
97
+ translateY.value = 0;
98
+ isFloating.current = false;
87
99
  };
88
100
  }, [
89
101
  duration,
@@ -92,12 +104,10 @@ export const useFloatAnimation = ({
92
104
  closeDuration,
93
105
  floatDistance,
94
106
  floatDuration,
107
+ isActive,
95
108
  ]);
96
109
 
97
110
  return {
98
- animatedStyle: {
99
- opacity,
100
- transform: [{ translateY }],
101
- },
111
+ animatedStyle,
102
112
  };
103
113
  };