@hoddy-ui/core 1.1.0 → 1.1.2

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/next/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from "../src/Components/OTPInput";
25
25
  // export * from "../src/config";
26
26
  export * from "../src/hooks";
27
27
  export * from "../src/theme";
28
+ export * from "../src/types";
28
29
 
29
30
  const HoddyUI = {
30
31
  initialize: initialize,
package/next/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoddy-ui/next",
3
- "version": "2.0.34",
3
+ "version": "2.0.47",
4
4
  "description": "Core rich react native components written in typescript, with support for expo-router",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoddy-ui/core",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Core rich react native components written in typescript",
5
5
  "main": "index.ts",
6
6
  "repository": {
@@ -0,0 +1,117 @@
1
+ import React, { FC } from "react";
2
+ import { Animated } from "react-native";
3
+ import { AnimatorProps } from "../../types";
4
+ import { useBlinkAnimation } from "./hooks/useBlinkAnimation";
5
+ import { useFadeAnimation } from "./hooks/useFadeAnimation";
6
+ import { useFloatAnimation } from "./hooks/useFloatAnimation";
7
+ import { useGrowAnimation } from "./hooks/useGrowAnimation";
8
+ import { useRollAnimation } from "./hooks/useRollAnimation";
9
+ import { useSlideAnimation } from "./hooks/useSlideAnimation";
10
+ import { useThrownUpAnimation } from "./hooks/useThrownUpAnimation";
11
+
12
+ /**
13
+ * Unified Animator component that handles multiple animation types with type-safe props.
14
+ *
15
+ * Each animation type only accepts its relevant props, ensuring type safety and better developer experience.
16
+ *
17
+ * @example
18
+ * // Fade animation - only accepts base props
19
+ * <Animator type="fade" duration={1000} closeAfter={3000}>
20
+ * <Text>This will fade in and out</Text>
21
+ * </Animator>
22
+ *
23
+ * @example
24
+ * // Slide animation - only accepts direction and initialValue props
25
+ * <Animator type="slide" direction="up" duration={800} closeAfter={2000}>
26
+ * <View>This will slide up from bottom</View>
27
+ * </Animator>
28
+ *
29
+ * @example
30
+ * // Grow animation - only accepts initialScale prop
31
+ * <Animator type="grow" initialScale={0.5} duration={600}>
32
+ * <Button>This will grow from 50% scale</Button>
33
+ * </Animator>
34
+ *
35
+ * @example
36
+ * // Blink animation - only accepts blink-specific props
37
+ * <Animator type="blink" blinkDuration={1000} minOpacity={0.3}>
38
+ * <Icon>This will blink continuously</Icon>
39
+ * </Animator>
40
+ *
41
+ * @example
42
+ * // TypeScript will show errors for invalid prop combinations:
43
+ * // ❌ This will cause a TypeScript error:
44
+ * // <Animator type="fade" direction="up"> // direction is not valid for fade
45
+ * //
46
+ * // ✅ This is correct:
47
+ * // <Animator type="slide" direction="up">
48
+ */
49
+ const Animator: FC<AnimatorProps> = (props) => {
50
+ const { children, type, duration, delay, closeAfter, style = {} } = props;
51
+
52
+ // Get animation style based on type
53
+ const getAnimationStyle = () => {
54
+ switch (type) {
55
+ case "fade":
56
+ return useFadeAnimation({ duration, delay, closeAfter });
57
+
58
+ case "grow":
59
+ return useGrowAnimation({
60
+ duration,
61
+ delay,
62
+ closeAfter,
63
+ initialScale: props.initialScale,
64
+ });
65
+
66
+ case "slide":
67
+ return useSlideAnimation({
68
+ duration,
69
+ delay,
70
+ direction: props.direction,
71
+ closeAfter,
72
+ initialValue: props.initialValue,
73
+ });
74
+
75
+ case "blink":
76
+ return useBlinkAnimation({
77
+ delay,
78
+ blinkDuration: props.blinkDuration,
79
+ minOpacity: props.minOpacity,
80
+ maxOpacity: props.maxOpacity,
81
+ });
82
+
83
+ case "float":
84
+ return useFloatAnimation({
85
+ duration,
86
+ delay,
87
+ closeAfter,
88
+ closeDuration: props.closeDuration,
89
+ floatDistance: props.floatDistance,
90
+ floatDuration: props.floatDuration,
91
+ });
92
+
93
+ case "roll":
94
+ return useRollAnimation({
95
+ duration,
96
+ delay,
97
+ closeAfter,
98
+ initialTranslateY: props.initialTranslateY,
99
+ initialRotate: props.initialRotate,
100
+ });
101
+
102
+ case "thrownup":
103
+ return useThrownUpAnimation({ delay, closeAfter });
104
+
105
+ default:
106
+ return { animatedStyle: {} };
107
+ }
108
+ };
109
+
110
+ const { animatedStyle } = getAnimationStyle();
111
+
112
+ return (
113
+ <Animated.View style={[style, animatedStyle]}>{children}</Animated.View>
114
+ );
115
+ };
116
+
117
+ export default Animator;
@@ -0,0 +1,137 @@
1
+ # Unified Animator Component
2
+
3
+ The `Animator` component provides a single interface for all animation types with generic props.
4
+
5
+ ## Features
6
+
7
+ - **Single Component**: One component handles all animation types
8
+ - **Generic Props**: Consistent prop naming across all animations (e.g., `closeAfter` instead of animation-specific names)
9
+ - **Modular Hooks**: Animation logic is separated into individual custom hooks
10
+ - **Type Safety**: Full TypeScript support with proper typing
11
+ - **Performance**: Only the specified animation runs, others are not loaded
12
+
13
+ ## Basic Usage
14
+
15
+ ```tsx
16
+ import { Animator } from 'hoddy-ui';
17
+
18
+ // Fade animation
19
+ <Animator type="fade" duration={1000} closeAfter={3000}>
20
+ <Text>This will fade in and out</Text>
21
+ </Animator>
22
+
23
+ // Slide animation
24
+ <Animator type="slide" direction="up" duration={800} closeAfter={2000}>
25
+ <View>This will slide up from bottom</View>
26
+ </Animator>
27
+
28
+ // Grow animation
29
+ <Animator type="grow" initialScale={0.5} duration={600}>
30
+ <Button>This will grow from 50% scale</Button>
31
+ </Animator>
32
+
33
+ // Blink animation (continuous)
34
+ <Animator type="blink" blinkDuration={1000} minOpacity={0.3}>
35
+ <Icon>This will blink continuously</Icon>
36
+ </Animator>
37
+
38
+ // Float animation
39
+ <Animator type="float" floatDistance={20} floatDuration={2000} closeAfter={5000}>
40
+ <View>This will float up and down</View>
41
+ </Animator>
42
+
43
+ // Roll animation
44
+ <Animator type="roll" initialRotate="45deg" duration={800}>
45
+ <Image>This will roll and rotate</Image>
46
+ </Animator>
47
+
48
+ // Thrown up animation
49
+ <Animator type="thrownup" delay={500} closeAfter={4000}>
50
+ <Notification>This will spring up from bottom</Notification>
51
+ </Animator>
52
+ ```
53
+
54
+ ## Generic Props
55
+
56
+ All animation types support these generic props:
57
+
58
+ - `type`: Animation type ("fade" | "grow" | "slide" | "blink" | "float" | "roll" | "thrownup")
59
+ - `duration`: Animation duration in milliseconds
60
+ - `delay`: Delay before animation starts
61
+ - `closeAfter`: Time after which the exit animation starts (null for no exit)
62
+ - `style`: Additional styles for the animated view
63
+
64
+ ## Animation-Specific Props
65
+
66
+ ### Slide Animation
67
+
68
+ - `direction`: "up" | "down" | "left" | "right"
69
+ - `initialValue`: Custom initial position value
70
+
71
+ ### Grow Animation
72
+
73
+ - `initialScale`: Starting scale (default: 0)
74
+
75
+ ### Blink Animation
76
+
77
+ - `blinkDuration`: Duration of one blink cycle
78
+ - `minOpacity`: Minimum opacity value
79
+ - `maxOpacity`: Maximum opacity value
80
+
81
+ ### Float Animation
82
+
83
+ - `closeDuration`: Duration of exit animation
84
+ - `floatDistance`: Distance to float up/down
85
+ - `floatDuration`: Duration of one float cycle
86
+
87
+ ### Roll Animation
88
+
89
+ - `initialTranslateY`: Initial vertical position
90
+ - `initialRotate`: Initial rotation value
91
+
92
+ ## Custom Hooks
93
+
94
+ You can also use the animation hooks directly:
95
+
96
+ ```tsx
97
+ import { useFadeAnimation, useSlideAnimation } from "hoddy-ui";
98
+
99
+ const MyComponent = () => {
100
+ const { animatedStyle } = useFadeAnimation({
101
+ duration: 800,
102
+ closeAfter: 2000,
103
+ });
104
+
105
+ return (
106
+ <Animated.View style={animatedStyle}>
107
+ <Text>Custom animated content</Text>
108
+ </Animated.View>
109
+ );
110
+ };
111
+ ```
112
+
113
+ ## Migration from Old Components
114
+
115
+ Replace old individual animation components:
116
+
117
+ ```tsx
118
+ // Old way
119
+ <AnimatedFade fadeOutAfter={2000}>
120
+ <Text>Content</Text>
121
+ </AnimatedFade>
122
+
123
+ // New way
124
+ <Animator type="fade" closeAfter={2000}>
125
+ <Text>Content</Text>
126
+ </Animator>
127
+ ```
128
+
129
+ ## Available Animation Types
130
+
131
+ 1. **fade**: Simple fade in/out
132
+ 2. **grow**: Scale-based growth animation
133
+ 3. **slide**: Directional slide animations
134
+ 4. **blink**: Continuous opacity blinking
135
+ 5. **float**: Floating up/down motion with fade
136
+ 6. **roll**: Combined rotation and translation
137
+ 7. **thrownup**: Spring-based upward animation
@@ -0,0 +1,8 @@
1
+ export { default as useAppState } from "./useAppState";
2
+ export { useBlinkAnimation } from "./useBlinkAnimation";
3
+ export { useFadeAnimation } from "./useFadeAnimation";
4
+ export { useFloatAnimation } from "./useFloatAnimation";
5
+ export { useGrowAnimation } from "./useGrowAnimation";
6
+ export { useRollAnimation } from "./useRollAnimation";
7
+ export { useSlideAnimation } from "./useSlideAnimation";
8
+ export { useThrownUpAnimation } from "./useThrownUpAnimation";
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from "react";
2
+ import { AppState, Platform } from "react-native";
3
+
4
+ const useAppState = () => {
5
+ const [isActive, setIsActive] = useState(true);
6
+
7
+ useEffect(() => {
8
+ const handleAppStateChange = (nextAppState: string) => {
9
+ setIsActive(nextAppState === "active");
10
+ };
11
+
12
+ let subscription: any;
13
+ let blurSub: any;
14
+ let focusSub: any;
15
+
16
+ if (Platform.OS === "android") {
17
+ blurSub = AppState.addEventListener("blur", () =>
18
+ handleAppStateChange("inactive")
19
+ );
20
+ focusSub = AppState.addEventListener("focus", () =>
21
+ handleAppStateChange("active")
22
+ );
23
+ } else {
24
+ subscription = AppState.addEventListener("change", handleAppStateChange);
25
+ }
26
+
27
+ return () => {
28
+ subscription?.remove();
29
+ blurSub?.remove();
30
+ focusSub?.remove();
31
+ };
32
+ }, []);
33
+
34
+ return { isActive };
35
+ };
36
+
37
+ export default useAppState;
@@ -0,0 +1,71 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated, Easing, Platform } from "react-native";
3
+ import useAppState from "./useAppState";
4
+
5
+ interface UseBlinkAnimationProps {
6
+ delay?: number;
7
+ blinkDuration?: number;
8
+ minOpacity?: number;
9
+ maxOpacity?: number;
10
+ }
11
+
12
+ export const useBlinkAnimation = ({
13
+ delay = 0,
14
+ blinkDuration = 2000,
15
+ minOpacity = 0.5,
16
+ maxOpacity = 1,
17
+ }: UseBlinkAnimationProps = {}) => {
18
+ const opacity = useRef(new Animated.Value(maxOpacity)).current;
19
+ const { isActive } = useAppState();
20
+ const blinkAnim = useRef<Animated.CompositeAnimation | null>(null);
21
+
22
+ const startBlinking = () => {
23
+ blinkAnim.current = Animated.loop(
24
+ Animated.sequence([
25
+ Animated.timing(opacity, {
26
+ toValue: minOpacity,
27
+ duration: blinkDuration / 2,
28
+ easing: Easing.inOut(Easing.quad),
29
+ useNativeDriver: true,
30
+ }),
31
+ Animated.timing(opacity, {
32
+ toValue: maxOpacity,
33
+ duration: blinkDuration / 2,
34
+ easing: Easing.inOut(Easing.quad),
35
+ useNativeDriver: true,
36
+ }),
37
+ ])
38
+ );
39
+ blinkAnim.current.start();
40
+ };
41
+
42
+ useEffect(() => {
43
+ if (!isActive && Platform.OS === "ios") {
44
+ opacity.stopAnimation();
45
+ }
46
+ }, [isActive]);
47
+
48
+ useEffect(() => {
49
+ if (delay > 0) {
50
+ const timer = setTimeout(() => {
51
+ startBlinking();
52
+ }, delay);
53
+ return () => {
54
+ clearTimeout(timer);
55
+ opacity.stopAnimation();
56
+ blinkAnim.current?.stop();
57
+ };
58
+ } else {
59
+ startBlinking();
60
+ }
61
+
62
+ return () => {
63
+ opacity.stopAnimation();
64
+ blinkAnim.current?.stop();
65
+ };
66
+ }, [delay, blinkDuration, minOpacity, maxOpacity]);
67
+
68
+ return {
69
+ animatedStyle: { opacity },
70
+ };
71
+ };
@@ -0,0 +1,50 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated, Platform } from "react-native";
3
+ import useAppState from "./useAppState";
4
+
5
+ interface UseFadeAnimationProps {
6
+ duration?: number;
7
+ delay?: number;
8
+ closeAfter?: number | null;
9
+ }
10
+
11
+ export const useFadeAnimation = ({
12
+ duration = 1000,
13
+ delay = 0,
14
+ closeAfter = 2000,
15
+ }: UseFadeAnimationProps = {}) => {
16
+ const opacity = useRef(new Animated.Value(0)).current;
17
+ const { isActive } = useAppState();
18
+
19
+ useEffect(() => {
20
+ if (!isActive && Platform.OS === "ios") {
21
+ opacity.stopAnimation();
22
+ }
23
+ }, [isActive]);
24
+
25
+ useEffect(() => {
26
+ // Fade-in animation
27
+ Animated.timing(opacity, {
28
+ toValue: 1,
29
+ duration,
30
+ 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]);
46
+
47
+ return {
48
+ animatedStyle: { opacity },
49
+ };
50
+ };
@@ -0,0 +1,103 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated, Easing, Platform } from "react-native";
3
+ import useAppState from "./useAppState";
4
+
5
+ interface UseFloatAnimationProps {
6
+ duration?: number;
7
+ delay?: number;
8
+ closeAfter?: number | null;
9
+ closeDuration?: number;
10
+ floatDistance?: number;
11
+ floatDuration?: number;
12
+ }
13
+
14
+ export const useFloatAnimation = ({
15
+ duration = 800,
16
+ delay = 0,
17
+ closeAfter = 2000,
18
+ closeDuration = 600,
19
+ floatDistance = 10,
20
+ floatDuration = 1200,
21
+ }: UseFloatAnimationProps = {}) => {
22
+ const opacity = useRef(new Animated.Value(0)).current;
23
+ const translateY = useRef(new Animated.Value(0)).current;
24
+ const { isActive } = useAppState();
25
+ const floatAnim = useRef<Animated.CompositeAnimation | null>(null);
26
+
27
+ 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();
51
+ };
52
+
53
+ useEffect(() => {
54
+ if (!isActive && Platform.OS === "ios") {
55
+ opacity.stopAnimation();
56
+ translateY.stopAnimation();
57
+ }
58
+ }, [isActive]);
59
+
60
+ useEffect(() => {
61
+ // Fade-in
62
+ Animated.timing(opacity, {
63
+ toValue: 1,
64
+ duration,
65
+ delay,
66
+ useNativeDriver: true,
67
+ }).start(() => {
68
+ startFloating();
69
+
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
+ });
82
+
83
+ return () => {
84
+ opacity.stopAnimation();
85
+ translateY.stopAnimation();
86
+ floatAnim.current?.stop();
87
+ };
88
+ }, [
89
+ duration,
90
+ delay,
91
+ closeAfter,
92
+ closeDuration,
93
+ floatDistance,
94
+ floatDuration,
95
+ ]);
96
+
97
+ return {
98
+ animatedStyle: {
99
+ opacity,
100
+ transform: [{ translateY }],
101
+ },
102
+ };
103
+ };
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated, Easing, Platform } from "react-native";
3
+ import useAppState from "./useAppState";
4
+
5
+ interface UseGrowAnimationProps {
6
+ duration?: number;
7
+ delay?: number;
8
+ closeAfter?: number | null;
9
+ initialScale?: number;
10
+ }
11
+
12
+ export const useGrowAnimation = ({
13
+ duration = 500,
14
+ delay = 0,
15
+ closeAfter = 2000,
16
+ initialScale = 0,
17
+ }: UseGrowAnimationProps = {}) => {
18
+ const scale = useRef(new Animated.Value(initialScale)).current;
19
+ const { isActive } = useAppState();
20
+
21
+ useEffect(() => {
22
+ if (!isActive && Platform.OS === "ios") {
23
+ scale.stopAnimation();
24
+ }
25
+ }, [isActive]);
26
+
27
+ useEffect(() => {
28
+ // Start grow-in animation with easing
29
+ Animated.timing(scale, {
30
+ toValue: 1,
31
+ duration,
32
+ delay,
33
+ easing: Easing.out(Easing.ease),
34
+ useNativeDriver: true,
35
+ }).start(() => {
36
+ if (closeAfter) {
37
+ setTimeout(() => {
38
+ Animated.timing(scale, {
39
+ toValue: initialScale,
40
+ duration,
41
+ easing: Easing.out(Easing.ease),
42
+ useNativeDriver: true,
43
+ }).start();
44
+ }, closeAfter);
45
+ }
46
+ });
47
+
48
+ return () => scale.stopAnimation();
49
+ }, [scale, duration, delay, closeAfter, initialScale]);
50
+
51
+ return {
52
+ animatedStyle: { transform: [{ scale }] },
53
+ };
54
+ };
@@ -0,0 +1,86 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated, Easing, Platform } from "react-native";
3
+ import useAppState from "./useAppState";
4
+
5
+ interface UseRollAnimationProps {
6
+ duration?: number;
7
+ delay?: number;
8
+ closeAfter?: number | null;
9
+ initialTranslateY?: number;
10
+ initialRotate?: string;
11
+ }
12
+
13
+ export const useRollAnimation = ({
14
+ duration = 500,
15
+ delay = 0,
16
+ closeAfter = 2000,
17
+ initialTranslateY = 100,
18
+ initialRotate = "0deg",
19
+ }: UseRollAnimationProps = {}) => {
20
+ const translateY = useRef(new Animated.Value(initialTranslateY)).current;
21
+ const rotate = useRef(new Animated.Value(0.5)).current;
22
+ const { isActive } = useAppState();
23
+
24
+ useEffect(() => {
25
+ if (!isActive && Platform.OS === "ios") {
26
+ rotate.stopAnimation();
27
+ translateY.stopAnimation();
28
+ }
29
+ }, [isActive]);
30
+
31
+ useEffect(() => {
32
+ // Start roll-in animation with easing
33
+ Animated.parallel([
34
+ Animated.timing(translateY, {
35
+ toValue: 0.5,
36
+ duration,
37
+ delay,
38
+ easing: Easing.out(Easing.ease),
39
+ useNativeDriver: true,
40
+ }),
41
+ Animated.timing(rotate, {
42
+ toValue: 1,
43
+ duration,
44
+ delay,
45
+ easing: Easing.out(Easing.ease),
46
+ useNativeDriver: true,
47
+ }),
48
+ ]).start(() => {
49
+ if (closeAfter) {
50
+ setTimeout(() => {
51
+ Animated.parallel([
52
+ Animated.timing(translateY, {
53
+ toValue: initialTranslateY,
54
+ duration,
55
+ easing: Easing.out(Easing.ease),
56
+ useNativeDriver: true,
57
+ }),
58
+ Animated.timing(rotate, {
59
+ toValue: 0,
60
+ duration,
61
+ easing: Easing.out(Easing.ease),
62
+ useNativeDriver: true,
63
+ }),
64
+ ]).start();
65
+ }, closeAfter);
66
+ }
67
+ });
68
+
69
+ return () => {
70
+ translateY.stopAnimation();
71
+ rotate.stopAnimation();
72
+ };
73
+ }, [translateY, rotate, duration, delay, closeAfter, initialTranslateY]);
74
+
75
+ // Interpolate the rotation value to degrees
76
+ const rotateInterpolation = rotate.interpolate({
77
+ inputRange: [0, 1],
78
+ outputRange: [initialRotate, "360deg"],
79
+ });
80
+
81
+ return {
82
+ animatedStyle: {
83
+ transform: [{ translateY }, { rotate: rotateInterpolation }],
84
+ },
85
+ };
86
+ };