cnnative-ui 1.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.
Files changed (175) hide show
  1. package/README.md +34 -0
  2. package/babel.config.js +6 -0
  3. package/jest.config.js +22 -0
  4. package/jest.init.js +5 -0
  5. package/jest.setup.js +173 -0
  6. package/package.json +87 -0
  7. package/src/__tests__/a11y/accessibility.test.tsx +33 -0
  8. package/src/__tests__/components/badge.test.tsx +25 -0
  9. package/src/__tests__/components/button.test.tsx +53 -0
  10. package/src/__tests__/components/card.test.tsx +28 -0
  11. package/src/__tests__/components/input.test.tsx +33 -0
  12. package/src/__tests__/hooks/use-controllable.test.ts +58 -0
  13. package/src/__tests__/integration.test.tsx +35 -0
  14. package/src/__tests__/lib/utils.test.ts +23 -0
  15. package/src/__tests__/mocks/handlers.ts +19 -0
  16. package/src/components/accordion/accordion.tsx +143 -0
  17. package/src/components/accordion/index.ts +1 -0
  18. package/src/components/alert/alert.tsx +65 -0
  19. package/src/components/alert/index.ts +1 -0
  20. package/src/components/alert-dialog/alert-dialog.tsx +145 -0
  21. package/src/components/alert-dialog/index.ts +1 -0
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +18 -0
  23. package/src/components/aspect-ratio/index.ts +1 -0
  24. package/src/components/avatar/avatar.tsx +93 -0
  25. package/src/components/avatar/index.ts +1 -0
  26. package/src/components/badge/badge.tsx +64 -0
  27. package/src/components/badge/index.ts +1 -0
  28. package/src/components/breadcrumb/breadcrumb.tsx +75 -0
  29. package/src/components/breadcrumb/index.ts +1 -0
  30. package/src/components/button/button.tsx +119 -0
  31. package/src/components/button/index.ts +1 -0
  32. package/src/components/card/card.tsx +40 -0
  33. package/src/components/card/index.ts +1 -0
  34. package/src/components/checkbox/checkbox.tsx +87 -0
  35. package/src/components/checkbox/index.ts +1 -0
  36. package/src/components/collapsible/collapsible.tsx +92 -0
  37. package/src/components/collapsible/index.ts +1 -0
  38. package/src/components/context-menu/context-menu.tsx +121 -0
  39. package/src/components/context-menu/index.ts +1 -0
  40. package/src/components/dialog/dialog.tsx +124 -0
  41. package/src/components/dialog/index.ts +1 -0
  42. package/src/components/dropdown-menu/dropdown-menu.tsx +145 -0
  43. package/src/components/dropdown-menu/index.ts +1 -0
  44. package/src/components/form/form.tsx +84 -0
  45. package/src/components/form/index.ts +1 -0
  46. package/src/components/input/index.ts +1 -0
  47. package/src/components/input/input.tsx +115 -0
  48. package/src/components/label/index.ts +1 -0
  49. package/src/components/label/label.tsx +13 -0
  50. package/src/components/navigation-menu/index.ts +1 -0
  51. package/src/components/navigation-menu/navigation-menu.tsx +68 -0
  52. package/src/components/pagination/index.ts +1 -0
  53. package/src/components/pagination/pagination.tsx +70 -0
  54. package/src/components/progress/index.ts +1 -0
  55. package/src/components/progress/progress.tsx +66 -0
  56. package/src/components/radio-group/index.ts +1 -0
  57. package/src/components/radio-group/radio-group.tsx +90 -0
  58. package/src/components/scroll-area/index.ts +1 -0
  59. package/src/components/scroll-area/scroll-area.tsx +27 -0
  60. package/src/components/select/index.ts +1 -0
  61. package/src/components/select/select.tsx +154 -0
  62. package/src/components/separator/index.ts +1 -0
  63. package/src/components/separator/separator.tsx +37 -0
  64. package/src/components/sheet/index.ts +1 -0
  65. package/src/components/sheet/sheet.tsx +128 -0
  66. package/src/components/skeleton/index.ts +1 -0
  67. package/src/components/skeleton/skeleton.tsx +84 -0
  68. package/src/components/slider/index.ts +1 -0
  69. package/src/components/slider/slider.tsx +145 -0
  70. package/src/components/switch/index.ts +1 -0
  71. package/src/components/switch/switch.tsx +78 -0
  72. package/src/components/table/index.ts +1 -0
  73. package/src/components/table/table.tsx +71 -0
  74. package/src/components/tabs/index.ts +1 -0
  75. package/src/components/tabs/tabs.tsx +124 -0
  76. package/src/components/textarea/index.ts +1 -0
  77. package/src/components/textarea/textarea.tsx +83 -0
  78. package/src/components/toast/index.ts +1 -0
  79. package/src/components/toast/toast.tsx +124 -0
  80. package/src/components/toggle/index.ts +1 -0
  81. package/src/components/toggle/toggle.tsx +87 -0
  82. package/src/components/toggle-group/index.ts +1 -0
  83. package/src/components/toggle-group/toggle-group.tsx +87 -0
  84. package/src/components/tooltip/index.ts +1 -0
  85. package/src/components/tooltip/tooltip.tsx +103 -0
  86. package/src/components/typography/index.ts +1 -0
  87. package/src/components/typography/typography.tsx +57 -0
  88. package/src/context/index.ts +3 -0
  89. package/src/context/provider.tsx +35 -0
  90. package/src/context/theme-context.tsx +81 -0
  91. package/src/context/toast-context.tsx +63 -0
  92. package/src/env.d.ts +2 -0
  93. package/src/hooks/index.ts +15 -0
  94. package/src/hooks/use-biometric.ts +27 -0
  95. package/src/hooks/use-color-scheme.ts +10 -0
  96. package/src/hooks/use-controllable.ts +40 -0
  97. package/src/hooks/use-countdown.ts +33 -0
  98. package/src/hooks/use-debounce.ts +18 -0
  99. package/src/hooks/use-disclosure.ts +14 -0
  100. package/src/hooks/use-haptics.ts +47 -0
  101. package/src/hooks/use-keyboard.ts +35 -0
  102. package/src/hooks/use-media-query.ts +27 -0
  103. package/src/hooks/use-press-animation.ts +45 -0
  104. package/src/hooks/use-previous.ts +14 -0
  105. package/src/hooks/use-scroll-header.ts +42 -0
  106. package/src/hooks/use-spring.ts +18 -0
  107. package/src/hooks/use-theme.ts +6 -0
  108. package/src/hooks/use-toast.ts +6 -0
  109. package/src/index.ts +53 -0
  110. package/src/lib/create-animated.tsx +25 -0
  111. package/src/lib/create-component.tsx +56 -0
  112. package/src/lib/index.ts +4 -0
  113. package/src/lib/platform.ts +25 -0
  114. package/src/lib/types.ts +28 -0
  115. package/src/lib/utils.ts +35 -0
  116. package/src/lib/variants.ts +7 -0
  117. package/src/premium/ai/chat-bubble.tsx +58 -0
  118. package/src/premium/ai/typing-indicator.tsx +59 -0
  119. package/src/premium/charts/bar-chart.tsx +66 -0
  120. package/src/premium/charts/progress-ring.tsx +63 -0
  121. package/src/premium/glass/glass-bottom-sheet.tsx +50 -0
  122. package/src/premium/glass/glass-card.tsx +51 -0
  123. package/src/premium/glass/glass-header.tsx +61 -0
  124. package/src/premium/glass/glass-panel.tsx +32 -0
  125. package/src/premium/glass/glass-sidebar.tsx +56 -0
  126. package/src/premium/index.ts +44 -0
  127. package/src/premium/index2.ts +13 -0
  128. package/src/premium/index3.ts +1 -0
  129. package/src/premium/inputs/color-picker.tsx +92 -0
  130. package/src/premium/inputs/currency-input.tsx +50 -0
  131. package/src/premium/inputs/otp-input.tsx +92 -0
  132. package/src/premium/inputs/phone-input.tsx +58 -0
  133. package/src/premium/inputs/rating.tsx +51 -0
  134. package/src/premium/layout/carousel.tsx +57 -0
  135. package/src/premium/layout/floating-dock.tsx +63 -0
  136. package/src/premium/layout/masonry-grid.tsx +41 -0
  137. package/src/premium/layout/parallax-scroll.tsx +81 -0
  138. package/src/premium/magic/animated-number.tsx +104 -0
  139. package/src/premium/magic/bento-grid.tsx +55 -0
  140. package/src/premium/magic/border-beam.tsx +68 -0
  141. package/src/premium/magic/confetti.tsx +88 -0
  142. package/src/premium/magic/magic-card.tsx +65 -0
  143. package/src/premium/magic/meteors.tsx +95 -0
  144. package/src/premium/magic/ripple.tsx +70 -0
  145. package/src/premium/magic/shimmer.tsx +58 -0
  146. package/src/premium/magic/shiny-button.tsx +70 -0
  147. package/src/premium/mobile/biometric-button.tsx +82 -0
  148. package/src/premium/mobile/bottom-tab-bar.tsx +81 -0
  149. package/src/premium/mobile/fab.tsx +74 -0
  150. package/src/premium/mobile/haptic-pressable.tsx +53 -0
  151. package/src/premium/mobile/notification-badge.tsx +61 -0
  152. package/src/premium/mobile/pull-to-refresh.tsx +84 -0
  153. package/src/premium/mobile/scroll-header.tsx +57 -0
  154. package/src/premium/mobile/swipe-row.tsx +128 -0
  155. package/src/premium/mobile/swipeable-card-stack.tsx +121 -0
  156. package/src/premium/motion/blur-fade.tsx +51 -0
  157. package/src/premium/motion/fade-up.tsx +34 -0
  158. package/src/premium/motion/marquee.tsx +67 -0
  159. package/src/premium/motion/pulsating-button.tsx +95 -0
  160. package/src/premium/motion/slide-in.tsx +38 -0
  161. package/src/premium/motion/stagger-children.tsx +28 -0
  162. package/src/premium/motion/typing-text.tsx +55 -0
  163. package/src/premium/motion/word-pull-up.tsx +34 -0
  164. package/src/premium/onboarding/step-indicator.tsx +65 -0
  165. package/src/tokens/colors.ts +83 -0
  166. package/src/tokens/global.css +83 -0
  167. package/src/tokens/index.ts +10 -0
  168. package/src/tokens/layout.ts +121 -0
  169. package/src/tokens/motion.ts +94 -0
  170. package/src/tokens/themes/dark.ts +7 -0
  171. package/src/tokens/themes/default.ts +8 -0
  172. package/src/tokens/themes/ocean.ts +28 -0
  173. package/src/tokens/themes/rose.ts +29 -0
  174. package/src/tokens/typography.ts +127 -0
  175. package/tsconfig.json +15 -0
@@ -0,0 +1,128 @@
1
+ import React from 'react';
2
+ import { View, Dimensions, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ withTiming,
8
+ runOnJS,
9
+ } from 'react-native-reanimated';
10
+ import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
11
+ import { useHaptics } from '../../hooks/use-haptics';
12
+ import { useSpring } from '../../hooks/use-spring';
13
+ import { cn } from '../../lib/utils';
14
+
15
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
16
+
17
+ export interface SwipeRowProps extends ViewProps {
18
+ leftActions?: React.ReactNode;
19
+ rightActions?: React.ReactNode;
20
+ leftThreshold?: number;
21
+ rightThreshold?: number;
22
+ onSwipeLeft?: () => void;
23
+ onSwipeRight?: () => void;
24
+ actionWidth?: number;
25
+ }
26
+
27
+ export const SwipeRow = React.forwardRef<React.ElementRef<typeof View>, SwipeRowProps>(
28
+ (
29
+ {
30
+ className,
31
+ children,
32
+ leftActions,
33
+ rightActions,
34
+ leftThreshold = SCREEN_WIDTH * 0.3,
35
+ rightThreshold = SCREEN_WIDTH * 0.3,
36
+ onSwipeLeft,
37
+ onSwipeRight,
38
+ actionWidth = 80,
39
+ style,
40
+ ...props
41
+ },
42
+ ref
43
+ ) => {
44
+ const translateX = useSharedValue(0);
45
+ const triggerHaptic = useHaptics();
46
+ const springConfig = useSpring('snappy');
47
+
48
+ const handleRelease = (currentX: number) => {
49
+ 'worklet';
50
+ if (rightActions && currentX < -rightThreshold) {
51
+ if (onSwipeLeft) runOnJS(onSwipeLeft)();
52
+ runOnJS(triggerHaptic)('success');
53
+ translateX.value = withSpring(-actionWidth, springConfig);
54
+ } else if (leftActions && currentX > leftThreshold) {
55
+ if (onSwipeRight) runOnJS(onSwipeRight)();
56
+ runOnJS(triggerHaptic)('success');
57
+ translateX.value = withSpring(actionWidth, springConfig);
58
+ } else {
59
+ translateX.value = withSpring(0, springConfig);
60
+ }
61
+ };
62
+
63
+ const panGestureEvent = (event: PanGestureHandlerGestureEvent) => {
64
+ 'worklet';
65
+ const translationX = event.nativeEvent.translationX;
66
+
67
+ // Limit swipe directions based on available actions
68
+ if (!leftActions && translationX > 0) return;
69
+ if (!rightActions && translationX < 0) return;
70
+
71
+ // Apply resistance when swiping past threshold
72
+ let newX = translationX;
73
+ if (translationX > leftThreshold) {
74
+ newX = leftThreshold + (translationX - leftThreshold) * 0.3;
75
+ } else if (translationX < -rightThreshold) {
76
+ newX = -rightThreshold + (translationX + rightThreshold) * 0.3;
77
+ }
78
+
79
+ translateX.value = newX;
80
+
81
+ if (event.nativeEvent.state === 5 || event.nativeEvent.state === 3) {
82
+ handleRelease(translationX);
83
+ }
84
+ };
85
+
86
+ const rStyle = useAnimatedStyle(() => {
87
+ return {
88
+ transform: [{ translateX: translateX.value }],
89
+ };
90
+ });
91
+
92
+ const leftActionStyle = useAnimatedStyle(() => {
93
+ return {
94
+ opacity: translateX.value > 0 ? 1 : 0,
95
+ transform: [{ translateX: (translateX.value - actionWidth) / 2 }],
96
+ };
97
+ });
98
+
99
+ const rightActionStyle = useAnimatedStyle(() => {
100
+ return {
101
+ opacity: translateX.value < 0 ? 1 : 0,
102
+ transform: [{ translateX: (translateX.value + actionWidth) / 2 }],
103
+ };
104
+ });
105
+
106
+ return (
107
+ <View ref={ref} className={cn('relative w-full overflow-hidden', className)} style={style} {...props}>
108
+ {/* Background Actions Layer */}
109
+ <View className="absolute inset-0 flex-row justify-between">
110
+ <Animated.View style={[{ width: actionWidth, height: '100%' }, leftActionStyle]}>
111
+ {leftActions}
112
+ </Animated.View>
113
+ <Animated.View style={[{ width: actionWidth, height: '100%' }, rightActionStyle]}>
114
+ {rightActions}
115
+ </Animated.View>
116
+ </View>
117
+
118
+ {/* Foreground Content Layer */}
119
+ <PanGestureHandler onGestureEvent={panGestureEvent as any}>
120
+ <Animated.View style={[rStyle, { width: '100%' }]} className="bg-background">
121
+ {children}
122
+ </Animated.View>
123
+ </PanGestureHandler>
124
+ </View>
125
+ );
126
+ }
127
+ );
128
+ SwipeRow.displayName = 'SwipeRow';
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+ import { View, Dimensions, StyleSheet } from 'react-native';
3
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
+ import Animated, {
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withSpring,
8
+ interpolate,
9
+ Extrapolate,
10
+ runOnJS,
11
+ } from 'react-native-reanimated';
12
+ import { useHaptics } from '../../hooks/use-haptics';
13
+
14
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
15
+ const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.3;
16
+
17
+ export interface SwipeableCardStackProps {
18
+ data: any[];
19
+ renderCard: (item: any, index: number) => React.ReactNode;
20
+ onSwipedLeft?: (item: any) => void;
21
+ onSwipedRight?: (item: any) => void;
22
+ onSwipedAll?: () => void;
23
+ }
24
+
25
+ export const SwipeableCardStack = ({ data, renderCard, onSwipedLeft, onSwipedRight, onSwipedAll }: SwipeableCardStackProps) => {
26
+ const [currentIndex, setCurrentIndex] = React.useState(0);
27
+ const translateX = useSharedValue(0);
28
+ const translateY = useSharedValue(0);
29
+ const triggerHaptic = useHaptics();
30
+
31
+ const handleSwipeComplete = (direction: 'left' | 'right') => {
32
+ const item = data[currentIndex];
33
+ if (direction === 'left') onSwipedLeft?.(item);
34
+ if (direction === 'right') onSwipedRight?.(item);
35
+
36
+ setCurrentIndex((prev) => {
37
+ const next = prev + 1;
38
+ if (next >= data.length) {
39
+ onSwipedAll?.();
40
+ }
41
+ return next;
42
+ });
43
+
44
+ translateX.value = 0;
45
+ translateY.value = 0;
46
+ };
47
+
48
+ const panGesture = Gesture.Pan()
49
+ .onUpdate((event) => {
50
+ translateX.value = event.translationX;
51
+ translateY.value = event.translationY;
52
+ })
53
+ .onEnd((event) => {
54
+ if (Math.abs(event.translationX) > SWIPE_THRESHOLD) {
55
+ // Swiped off screen
56
+ const isRight = event.translationX > 0;
57
+ runOnJS(triggerHaptic)('success');
58
+ translateX.value = withSpring(isRight ? SCREEN_WIDTH : -SCREEN_WIDTH, { velocity: event.velocityX }, () => {
59
+ runOnJS(handleSwipeComplete)(isRight ? 'right' : 'left');
60
+ });
61
+ } else {
62
+ // Return to center
63
+ runOnJS(triggerHaptic)('light');
64
+ translateX.value = withSpring(0);
65
+ translateY.value = withSpring(0);
66
+ }
67
+ });
68
+
69
+ const animatedCardStyle = useAnimatedStyle(() => {
70
+ const rotate = interpolate(translateX.value, [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2], [-10, 0, 10], Extrapolate.CLAMP);
71
+ return {
72
+ transform: [
73
+ { translateX: translateX.value },
74
+ { translateY: translateY.value },
75
+ { rotateZ: `${rotate}deg` },
76
+ ],
77
+ };
78
+ });
79
+
80
+ if (currentIndex >= data.length) {
81
+ return null; // or a finished state
82
+ }
83
+
84
+ return (
85
+ <View style={styles.container}>
86
+ {/* Next Card (Background) */}
87
+ {currentIndex + 1 < data.length && (
88
+ <Animated.View style={[styles.card, { transform: [{ scale: 0.95 }, { translateY: 20 }] }]}>
89
+ {renderCard(data[currentIndex + 1], currentIndex + 1)}
90
+ </Animated.View>
91
+ )}
92
+
93
+ {/* Current Card (Foreground) */}
94
+ <GestureDetector gesture={panGesture}>
95
+ <Animated.View style={[styles.card, animatedCardStyle]}>
96
+ {renderCard(data[currentIndex], currentIndex)}
97
+ </Animated.View>
98
+ </GestureDetector>
99
+ </View>
100
+ );
101
+ };
102
+
103
+ const styles = StyleSheet.create({
104
+ container: {
105
+ flex: 1,
106
+ alignItems: 'center',
107
+ justifyContent: 'center',
108
+ },
109
+ card: {
110
+ position: 'absolute',
111
+ width: '90%',
112
+ height: '70%',
113
+ borderRadius: 20,
114
+ backgroundColor: 'white',
115
+ shadowColor: '#000',
116
+ shadowOffset: { width: 0, height: 10 },
117
+ shadowOpacity: 0.1,
118
+ shadowRadius: 20,
119
+ elevation: 10,
120
+ },
121
+ });
@@ -0,0 +1,51 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withDelay,
7
+ withSpring,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
10
+ import { useSpring } from '../../hooks/use-spring';
11
+
12
+ export interface BlurFadeProps extends ViewProps {
13
+ delay?: number;
14
+ duration?: number;
15
+ yOffset?: number;
16
+ blurAmount?: number;
17
+ inView?: boolean;
18
+ }
19
+
20
+ export const BlurFade = React.forwardRef<React.ElementRef<typeof View>, BlurFadeProps>(
21
+ ({ className, children, delay = 0, duration = 400, yOffset = 24, blurAmount = 10, inView = true, style, ...props }, ref) => {
22
+ const opacity = useSharedValue(0);
23
+ const translateY = useSharedValue(yOffset);
24
+ // Note: True blur animation requires complex shader setups in React Native or Expo Blur
25
+ // We simulate the effect with opacity + transform which works incredibly well
26
+
27
+ const springConfig = useSpring('smooth');
28
+
29
+ useEffect(() => {
30
+ if (inView) {
31
+ opacity.value = withDelay(delay, withTiming(1, { duration }));
32
+ translateY.value = withDelay(delay, withSpring(0, springConfig));
33
+ } else {
34
+ opacity.value = withTiming(0, { duration });
35
+ translateY.value = withSpring(yOffset, springConfig);
36
+ }
37
+ }, [inView, delay, duration, yOffset, springConfig]);
38
+
39
+ const animatedStyle = useAnimatedStyle(() => ({
40
+ opacity: opacity.value,
41
+ transform: [{ translateY: translateY.value }],
42
+ }));
43
+
44
+ return (
45
+ <Animated.View ref={ref as any} style={[animatedStyle, style]} className={className} {...props}>
46
+ {children}
47
+ </Animated.View>
48
+ );
49
+ }
50
+ );
51
+ BlurFade.displayName = 'BlurFade';
@@ -0,0 +1,34 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming } from 'react-native-reanimated';
4
+
5
+ export interface FadeUpProps extends ViewProps {
6
+ delay?: number;
7
+ duration?: number;
8
+ distance?: number;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ export const FadeUp = React.forwardRef<React.ElementRef<typeof View>, FadeUpProps>(
13
+ ({ className, delay = 0, duration = 500, distance = 20, children, style, ...props }, ref) => {
14
+ const opacity = useSharedValue(0);
15
+ const translateY = useSharedValue(distance);
16
+
17
+ useEffect(() => {
18
+ opacity.value = withDelay(delay, withTiming(1, { duration }));
19
+ translateY.value = withDelay(delay, withSpring(0, { damping: 20, stiffness: 100 }));
20
+ }, [delay, duration, distance]);
21
+
22
+ const animatedStyle = useAnimatedStyle(() => ({
23
+ opacity: opacity.value,
24
+ transform: [{ translateY: translateY.value }],
25
+ }));
26
+
27
+ return (
28
+ <Animated.View ref={ref as any} style={[animatedStyle, style]} className={className} {...props}>
29
+ {children}
30
+ </Animated.View>
31
+ );
32
+ }
33
+ );
34
+ FadeUp.displayName = 'FadeUp';
@@ -0,0 +1,67 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View, type ViewProps, Dimensions } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ cancelAnimation,
10
+ } from 'react-native-reanimated';
11
+ import { cn } from '../../lib/utils';
12
+
13
+ export interface MarqueeProps extends ViewProps {
14
+ duration?: number;
15
+ direction?: 'left' | 'right';
16
+ reverse?: boolean;
17
+ }
18
+
19
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
20
+
21
+ export const Marquee = React.forwardRef<React.ElementRef<typeof View>, MarqueeProps>(
22
+ ({ className, children, duration = 10000, direction = 'left', reverse = false, ...props }, ref) => {
23
+ const [contentWidth, setContentWidth] = useState(0);
24
+ const translateX = useSharedValue(0);
25
+
26
+ const actualDirection = reverse ? (direction === 'left' ? 'right' : 'left') : direction;
27
+
28
+ useEffect(() => {
29
+ if (contentWidth > 0) {
30
+ // Reset translation
31
+ translateX.value = actualDirection === 'left' ? 0 : -contentWidth;
32
+
33
+ translateX.value = withRepeat(
34
+ withTiming(actualDirection === 'left' ? -contentWidth : 0, {
35
+ duration: duration,
36
+ easing: Easing.linear,
37
+ }),
38
+ -1, // Infinite loop
39
+ false // No reverse
40
+ );
41
+ }
42
+ return () => cancelAnimation(translateX);
43
+ }, [contentWidth, actualDirection, duration]);
44
+
45
+ const animatedStyle = useAnimatedStyle(() => ({
46
+ transform: [{ translateX: translateX.value }],
47
+ }));
48
+
49
+ return (
50
+ <View ref={ref} className={cn('flex-row overflow-hidden w-full', className)} {...props}>
51
+ <Animated.View
52
+ style={[animatedStyle, { flexDirection: 'row' }]}
53
+ onLayout={(e) => setContentWidth(e.nativeEvent.layout.width)}
54
+ >
55
+ {children}
56
+ </Animated.View>
57
+ {/* Render a duplicate so it loops seamlessly */}
58
+ {contentWidth > 0 && (
59
+ <Animated.View style={[animatedStyle, { flexDirection: 'row', position: 'absolute', left: contentWidth }]}>
60
+ {children}
61
+ </Animated.View>
62
+ )}
63
+ </View>
64
+ );
65
+ }
66
+ );
67
+ Marquee.displayName = 'Marquee';
@@ -0,0 +1,95 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, Pressable, type PressableProps, type StyleProp, type ViewStyle } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ withSpring,
9
+ Easing,
10
+ } from 'react-native-reanimated';
11
+ import { useThemeContext } from '../../context/theme-context';
12
+ import { useHaptics } from '../../hooks/use-haptics';
13
+ import { cn } from '../../lib/utils';
14
+ import { Text } from '../../components/typography';
15
+
16
+ export interface PulsatingButtonProps extends Omit<PressableProps, 'style'> {
17
+ label: string;
18
+ pulseColor?: string;
19
+ style?: StyleProp<ViewStyle>;
20
+ className?: string;
21
+ }
22
+
23
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
24
+
25
+ export const PulsatingButton = React.forwardRef<React.ElementRef<typeof Pressable>, PulsatingButtonProps>(
26
+ ({ className, label, pulseColor, onPress, style, ...props }, ref) => {
27
+ const { theme } = useThemeContext();
28
+ const triggerHaptic = useHaptics();
29
+ const pulseScale = useSharedValue(1);
30
+ const pulseOpacity = useSharedValue(0.5);
31
+ const pressScale = useSharedValue(1);
32
+
33
+ const actualPulseColor = pulseColor || theme.primary.DEFAULT;
34
+
35
+ useEffect(() => {
36
+ pulseScale.value = withRepeat(
37
+ withTiming(1.5, { duration: 1500, easing: Easing.out(Easing.ease) }),
38
+ -1,
39
+ false
40
+ );
41
+ pulseOpacity.value = withRepeat(
42
+ withTiming(0, { duration: 1500, easing: Easing.out(Easing.ease) }),
43
+ -1,
44
+ false
45
+ );
46
+ }, []);
47
+
48
+ const ringStyle = useAnimatedStyle(() => ({
49
+ transform: [{ scale: pulseScale.value }],
50
+ opacity: pulseOpacity.value,
51
+ }));
52
+
53
+ const buttonStyle = useAnimatedStyle(() => ({
54
+ transform: [{ scale: pressScale.value }],
55
+ }));
56
+
57
+ const handlePressIn = () => {
58
+ pressScale.value = withSpring(0.95);
59
+ };
60
+
61
+ const handlePressOut = () => {
62
+ pressScale.value = withSpring(1);
63
+ };
64
+
65
+ return (
66
+ <View className={cn('relative items-center justify-center', className)} style={style}>
67
+ {/* Pulsating Ring */}
68
+ <Animated.View
69
+ style={[
70
+ { backgroundColor: actualPulseColor },
71
+ ringStyle,
72
+ ]}
73
+ className="absolute inset-0 rounded-full"
74
+ />
75
+
76
+ {/* Actual Button */}
77
+ <AnimatedPressable
78
+ ref={ref as any}
79
+ onPressIn={handlePressIn}
80
+ onPressOut={handlePressOut}
81
+ onPress={(e) => {
82
+ triggerHaptic('selection');
83
+ onPress?.(e);
84
+ }}
85
+ className="rounded-full bg-primary px-6 py-3 shadow-lg flex-row items-center justify-center z-10"
86
+ style={buttonStyle}
87
+ {...props}
88
+ >
89
+ <Text className="text-primary-foreground font-semibold text-base">{label}</Text>
90
+ </AnimatedPressable>
91
+ </View>
92
+ );
93
+ }
94
+ );
95
+ PulsatingButton.displayName = 'PulsatingButton';
@@ -0,0 +1,38 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, useSharedValue, withDelay, withSpring } from 'react-native-reanimated';
4
+
5
+ export interface SlideInProps extends ViewProps {
6
+ delay?: number;
7
+ direction?: 'left' | 'right' | 'up' | 'down';
8
+ distance?: number;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ export const SlideIn = React.forwardRef<React.ElementRef<typeof View>, SlideInProps>(
13
+ ({ className, delay = 0, direction = 'left', distance = 50, children, style, ...props }, ref) => {
14
+ const translation = useSharedValue(distance);
15
+
16
+ // Determine the axis and sign
17
+ const isX = direction === 'left' || direction === 'right';
18
+ const initialTranslate = (direction === 'left' || direction === 'up') ? distance : -distance;
19
+
20
+ useEffect(() => {
21
+ translation.value = initialTranslate;
22
+ translation.value = withDelay(delay, withSpring(0, { damping: 20, stiffness: 150 }));
23
+ }, [delay, direction, distance]);
24
+
25
+ const animatedStyle = useAnimatedStyle(() => ({
26
+ transform: [
27
+ isX ? { translateX: translation.value } : { translateY: translation.value }
28
+ ],
29
+ }));
30
+
31
+ return (
32
+ <Animated.View ref={ref as any} style={[animatedStyle, style]} className={className} {...props}>
33
+ {children}
34
+ </Animated.View>
35
+ );
36
+ }
37
+ );
38
+ SlideIn.displayName = 'SlideIn';
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { FadeUp } from './fade-up';
4
+
5
+ export interface StaggerChildrenProps extends ViewProps {
6
+ staggerDelay?: number;
7
+ initialDelay?: number;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export const StaggerChildren = React.forwardRef<React.ElementRef<typeof View>, StaggerChildrenProps>(
12
+ ({ className, staggerDelay = 100, initialDelay = 0, children, ...props }, ref) => {
13
+
14
+ // We expect children to be an array of React Elements
15
+ const childrenArray = React.Children.toArray(children);
16
+
17
+ return (
18
+ <View ref={ref} className={className} {...props}>
19
+ {childrenArray.map((child, index) => (
20
+ <FadeUp key={index} delay={initialDelay + index * staggerDelay}>
21
+ {child}
22
+ </FadeUp>
23
+ ))}
24
+ </View>
25
+ );
26
+ }
27
+ );
28
+ StaggerChildren.displayName = 'StaggerChildren';
@@ -0,0 +1,55 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { Text } from '../../components/typography';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ export interface TypingTextProps extends ViewProps {
7
+ text: string;
8
+ typingSpeed?: number;
9
+ cursor?: string;
10
+ cursorBlinkSpeed?: number;
11
+ }
12
+
13
+ export const TypingText = React.forwardRef<React.ElementRef<typeof View>, TypingTextProps>(
14
+ ({ className, text, typingSpeed = 50, cursor = '|', cursorBlinkSpeed = 500, style, ...props }, ref) => {
15
+ const [displayedText, setDisplayedText] = useState('');
16
+ const [showCursor, setShowCursor] = useState(true);
17
+ const [isTyping, setIsTyping] = useState(true);
18
+
19
+ useEffect(() => {
20
+ let i = 0;
21
+ setIsTyping(true);
22
+ setDisplayedText('');
23
+
24
+ const typingInterval = setInterval(() => {
25
+ if (i < text.length) {
26
+ setDisplayedText((prev) => prev + text.charAt(i));
27
+ i++;
28
+ } else {
29
+ clearInterval(typingInterval);
30
+ setIsTyping(false);
31
+ }
32
+ }, typingSpeed);
33
+
34
+ return () => clearInterval(typingInterval);
35
+ }, [text, typingSpeed]);
36
+
37
+ useEffect(() => {
38
+ const cursorInterval = setInterval(() => {
39
+ setShowCursor((prev) => !prev);
40
+ }, cursorBlinkSpeed);
41
+
42
+ return () => clearInterval(cursorInterval);
43
+ }, [cursorBlinkSpeed]);
44
+
45
+ return (
46
+ <View ref={ref} className={cn('flex-row items-center', className)} style={style} {...props}>
47
+ <Text className="text-foreground">{displayedText}</Text>
48
+ <Text className={cn('text-foreground opacity-100', !showCursor && 'opacity-0')}>
49
+ {cursor}
50
+ </Text>
51
+ </View>
52
+ );
53
+ }
54
+ );
55
+ TypingText.displayName = 'TypingText';
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { Text } from '../../components/typography';
4
+ import { FadeUp } from './fade-up';
5
+ import { cn } from '../../lib/utils';
6
+
7
+ export interface WordPullUpProps extends ViewProps {
8
+ text: string;
9
+ delay?: number;
10
+ wordDelay?: number;
11
+ }
12
+
13
+ export const WordPullUp = React.forwardRef<React.ElementRef<typeof View>, WordPullUpProps>(
14
+ ({ className, text, delay = 0, wordDelay = 100, style, ...props }, ref) => {
15
+ const words = text.split(' ');
16
+
17
+ return (
18
+ <View ref={ref} className={cn('flex-row flex-wrap', className)} style={style} {...props}>
19
+ {words.map((word, i) => (
20
+ <FadeUp
21
+ key={i}
22
+ delay={delay + i * wordDelay}
23
+ duration={500}
24
+ distance={20}
25
+ className="mr-1 mb-1"
26
+ >
27
+ <Text className="text-foreground font-medium text-lg">{word}</Text>
28
+ </FadeUp>
29
+ ))}
30
+ </View>
31
+ );
32
+ }
33
+ );
34
+ WordPullUp.displayName = 'WordPullUp';