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,63 @@
1
+ import React from 'react';
2
+ import { View, Pressable, type ViewProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
4
+ import { cn } from '../../lib/utils';
5
+ import { useHaptics } from '../../hooks/use-haptics';
6
+
7
+ export interface FloatingDockItem {
8
+ icon: React.ReactNode;
9
+ onPress: () => void;
10
+ label?: string;
11
+ }
12
+
13
+ export interface FloatingDockProps extends ViewProps {
14
+ items: FloatingDockItem[];
15
+ }
16
+
17
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
18
+
19
+ export const FloatingDock = React.forwardRef<React.ElementRef<typeof View>, FloatingDockProps>(
20
+ ({ className, items, ...props }, ref) => {
21
+ return (
22
+ <View
23
+ ref={ref}
24
+ className={cn('absolute bottom-8 self-center rounded-2xl bg-background/80 p-2 border border-border shadow-lg flex-row items-center gap-2', className)}
25
+ style={{ backdropFilter: 'blur(10px)' } as any}
26
+ {...props}
27
+ >
28
+ {items.map((item, index) => (
29
+ <DockItem key={index} item={item} />
30
+ ))}
31
+ </View>
32
+ );
33
+ }
34
+ );
35
+ FloatingDock.displayName = 'FloatingDock';
36
+
37
+ const DockItem = ({ item }: { item: FloatingDockItem }) => {
38
+ const triggerHaptic = useHaptics();
39
+ const scale = useSharedValue(1);
40
+ const translateY = useSharedValue(0);
41
+
42
+ const animatedStyle = useAnimatedStyle(() => ({
43
+ transform: [{ scale: scale.value }, { translateY: translateY.value }],
44
+ }));
45
+
46
+ return (
47
+ <AnimatedPressable
48
+ onPressIn={() => {
49
+ triggerHaptic('light');
50
+ scale.value = withSpring(1.2);
51
+ translateY.value = withSpring(-10);
52
+ }}
53
+ onPressOut={() => {
54
+ scale.value = withSpring(1);
55
+ translateY.value = withSpring(0);
56
+ }}
57
+ onPress={item.onPress}
58
+ className="h-12 w-12 items-center justify-center rounded-xl bg-accent hover:bg-accent/80 transition-colors"
59
+ >
60
+ {item.icon}
61
+ </AnimatedPressable>
62
+ );
63
+ };
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { View, ScrollView, type ViewProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+ import { FadeUp } from '../motion/fade-up';
5
+
6
+ export interface MasonryGridProps extends ViewProps {
7
+ data: any[];
8
+ renderItem: (item: any, index: number) => React.ReactNode;
9
+ columns?: number;
10
+ }
11
+
12
+ export const MasonryGrid = React.forwardRef<React.ElementRef<typeof ScrollView>, MasonryGridProps>(
13
+ ({ className, data, renderItem, columns = 2, ...props }, ref) => {
14
+
15
+ // Distribute items into columns
16
+ const columnsData = Array.from({ length: columns }, () => [] as any[]);
17
+ data.forEach((item, i) => {
18
+ const col = columnsData[i % columns];
19
+ if (col) {
20
+ col.push({ item, index: i });
21
+ }
22
+ });
23
+
24
+ return (
25
+ <ScrollView ref={ref as any} className={cn('flex-1', className)} {...props}>
26
+ <View className="flex-row items-start px-2 py-4 gap-2">
27
+ {columnsData.map((col, colIndex) => (
28
+ <View key={colIndex} className="flex-1 flex-col gap-2">
29
+ {col.map((wrapper) => (
30
+ <FadeUp key={wrapper.index} delay={wrapper.index * 50}>
31
+ {renderItem(wrapper.item, wrapper.index)}
32
+ </FadeUp>
33
+ ))}
34
+ </View>
35
+ ))}
36
+ </View>
37
+ </ScrollView>
38
+ );
39
+ }
40
+ );
41
+ MasonryGrid.displayName = 'MasonryGrid';
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, Dimensions } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedScrollHandler,
5
+ useSharedValue,
6
+ useAnimatedStyle,
7
+ interpolate,
8
+ Extrapolate,
9
+ } from 'react-native-reanimated';
10
+ import { cn } from '../../lib/utils';
11
+
12
+ export interface ParallaxScrollProps {
13
+ headerImage: React.ReactNode;
14
+ headerHeight?: number;
15
+ children: React.ReactNode;
16
+ className?: string;
17
+ }
18
+
19
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
20
+
21
+ export const ParallaxScroll = React.forwardRef<React.ElementRef<typeof Animated.ScrollView>, ParallaxScrollProps>(
22
+ ({ className, headerImage, headerHeight = 300, children, ...props }, ref) => {
23
+ const scrollY = useSharedValue(0);
24
+
25
+ const scrollHandler = useAnimatedScrollHandler({
26
+ onScroll: (event) => {
27
+ scrollY.value = event.contentOffset.y;
28
+ },
29
+ });
30
+
31
+ const headerAnimatedStyle = useAnimatedStyle(() => {
32
+ const translateY = interpolate(
33
+ scrollY.value,
34
+ [-headerHeight, 0, headerHeight],
35
+ [-headerHeight / 2, 0, headerHeight * 0.5],
36
+ Extrapolate.CLAMP
37
+ );
38
+ const scale = interpolate(
39
+ scrollY.value,
40
+ [-headerHeight, 0],
41
+ [2, 1],
42
+ Extrapolate.CLAMP
43
+ );
44
+
45
+ return {
46
+ transform: [{ translateY }, { scale }],
47
+ };
48
+ });
49
+
50
+ return (
51
+ <View className={cn('flex-1 bg-background', className)}>
52
+ <Animated.View style={[styles.header, { height: headerHeight }, headerAnimatedStyle]}>
53
+ {headerImage}
54
+ </Animated.View>
55
+ <Animated.ScrollView
56
+ ref={ref as any}
57
+ onScroll={scrollHandler}
58
+ scrollEventThrottle={16}
59
+ contentContainerStyle={{ paddingTop: headerHeight }}
60
+ {...props}
61
+ >
62
+ <View className="bg-background min-h-screen rounded-t-3xl -mt-6 p-6">
63
+ {children}
64
+ </View>
65
+ </Animated.ScrollView>
66
+ </View>
67
+ );
68
+ }
69
+ );
70
+ ParallaxScroll.displayName = 'ParallaxScroll';
71
+
72
+ const styles = StyleSheet.create({
73
+ header: {
74
+ position: 'absolute',
75
+ top: 0,
76
+ left: 0,
77
+ right: 0,
78
+ width: SCREEN_WIDTH,
79
+ overflow: 'hidden',
80
+ },
81
+ });
@@ -0,0 +1,104 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps, type TextStyle } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ interpolate,
8
+ } from 'react-native-reanimated';
9
+ import { useSpring } from '../../hooks/use-spring';
10
+ import { cn } from '../../lib/utils';
11
+ import { Text } from '../../components/typography';
12
+
13
+ export interface AnimatedNumberProps extends ViewProps {
14
+ value: number;
15
+ fontSize?: number;
16
+ textStyle?: TextStyle | string;
17
+ }
18
+
19
+ const NUMBERS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
20
+
21
+ /**
22
+ * Renders a single digit column that slides up/down
23
+ */
24
+ const AnimatedDigit = ({ digit, fontSize = 32, textStyle }: { digit: number, fontSize?: number, textStyle?: any }) => {
25
+ const translateY = useSharedValue(0);
26
+ const springConfig = useSpring('bouncy');
27
+
28
+ useEffect(() => {
29
+ translateY.value = withSpring(-digit * fontSize, springConfig);
30
+ }, [digit, fontSize, springConfig]);
31
+
32
+ const rStyle = useAnimatedStyle(() => {
33
+ return {
34
+ transform: [{ translateY: translateY.value }],
35
+ };
36
+ });
37
+
38
+ return (
39
+ <View style={{ height: fontSize, overflow: 'hidden' }}>
40
+ <Animated.View style={rStyle}>
41
+ {NUMBERS.map((num) => (
42
+ <Text
43
+ key={num}
44
+ style={[{ height: fontSize, lineHeight: fontSize, fontSize }, typeof textStyle === 'object' ? textStyle : {}]}
45
+ className={cn('text-center font-bold font-mono tracking-tighter', typeof textStyle === 'string' ? textStyle : '')}
46
+ >
47
+ {num}
48
+ </Text>
49
+ ))}
50
+ </Animated.View>
51
+ </View>
52
+ );
53
+ };
54
+
55
+ /**
56
+ * A Magic UI inspired Animated Number component.
57
+ * Rolls the digits into place like a slot machine when the value changes.
58
+ */
59
+ export const AnimatedNumber = React.forwardRef<React.ElementRef<typeof View>, AnimatedNumberProps>(
60
+ ({ value, fontSize = 48, textStyle, className, style, ...props }, ref) => {
61
+ const valueStr = Math.abs(value).toString();
62
+ const isNegative = value < 0;
63
+
64
+ return (
65
+ <View
66
+ ref={ref}
67
+ className={cn('flex-row items-center overflow-hidden', className)}
68
+ style={style}
69
+ {...props}
70
+ >
71
+ {isNegative && (
72
+ <Text
73
+ style={[{ fontSize, lineHeight: fontSize }, typeof textStyle === 'object' ? textStyle : {}]}
74
+ className={cn('font-bold font-mono tracking-tighter', typeof textStyle === 'string' ? textStyle : '')}
75
+ >
76
+ -
77
+ </Text>
78
+ )}
79
+ {valueStr.split('').map((char, index) => {
80
+ if (char === '.') {
81
+ return (
82
+ <Text
83
+ key={`dot-${index}`}
84
+ style={[{ fontSize, lineHeight: fontSize }, typeof textStyle === 'object' ? textStyle : {}]}
85
+ className={cn('font-bold font-mono tracking-tighter', typeof textStyle === 'string' ? textStyle : '')}
86
+ >
87
+ .
88
+ </Text>
89
+ );
90
+ }
91
+ return (
92
+ <AnimatedDigit
93
+ key={`${index}-${valueStr.length}`}
94
+ digit={parseInt(char, 10)}
95
+ fontSize={fontSize}
96
+ textStyle={textStyle}
97
+ />
98
+ );
99
+ })}
100
+ </View>
101
+ );
102
+ }
103
+ );
104
+ AnimatedNumber.displayName = 'AnimatedNumber';
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ export interface BentoGridProps extends ViewProps {}
6
+
7
+ export const BentoGrid = React.forwardRef<React.ElementRef<typeof View>, BentoGridProps>(
8
+ ({ className, children, ...props }, ref) => {
9
+ return (
10
+ <View
11
+ ref={ref}
12
+ className={cn(
13
+ 'flex flex-row flex-wrap gap-4 mx-auto w-full max-w-7xl',
14
+ className
15
+ )}
16
+ {...props}
17
+ >
18
+ {children}
19
+ </View>
20
+ );
21
+ }
22
+ );
23
+ BentoGrid.displayName = 'BentoGrid';
24
+
25
+ export interface BentoCardProps extends ViewProps {
26
+ colSpan?: 1 | 2 | 3 | 4;
27
+ }
28
+
29
+ export const BentoCard = React.forwardRef<React.ElementRef<typeof View>, BentoCardProps>(
30
+ ({ className, colSpan = 1, children, ...props }, ref) => {
31
+ // Very basic mapping for flex-basis based on a 4-column desktop grid
32
+ // For React Native, percentage widths work best. We'll subtract the gap roughly.
33
+ const basisMap = {
34
+ 1: 'basis-[23%]',
35
+ 2: 'basis-[48%]',
36
+ 3: 'basis-[73%]',
37
+ 4: 'basis-[100%]',
38
+ };
39
+
40
+ return (
41
+ <View
42
+ ref={ref}
43
+ className={cn(
44
+ 'flex-1 min-w-[280px] rounded-xl border border-border bg-card shadow-sm overflow-hidden p-6',
45
+ basisMap[colSpan],
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ </View>
52
+ );
53
+ }
54
+ );
55
+ BentoCard.displayName = 'BentoCard';
@@ -0,0 +1,68 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, StyleSheet, type ViewProps, Dimensions } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ interpolate,
10
+ } from 'react-native-reanimated';
11
+ import { LinearGradient } from 'expo-linear-gradient';
12
+
13
+ export interface BorderBeamProps extends ViewProps {
14
+ duration?: number;
15
+ color?: string;
16
+ size?: number;
17
+ }
18
+
19
+ export const BorderBeam = React.forwardRef<React.ElementRef<typeof View>, BorderBeamProps>(
20
+ ({ className, duration = 4000, color = 'hsl(var(--primary))', size = 50, style, ...props }, ref) => {
21
+ const perimeter = useSharedValue(0);
22
+
23
+ useEffect(() => {
24
+ perimeter.value = withRepeat(
25
+ withTiming(1, { duration, easing: Easing.linear }),
26
+ -1,
27
+ false
28
+ );
29
+ }, [duration]);
30
+
31
+ // This is a simplified border beam using a rotating gradient mask approach
32
+ const animatedStyle = useAnimatedStyle(() => ({
33
+ transform: [{ rotate: `${interpolate(perimeter.value, [0, 1], [0, 360])}deg` }],
34
+ }));
35
+
36
+ return (
37
+ <View
38
+ ref={ref}
39
+ className="absolute inset-0 z-10 overflow-hidden rounded-[inherit] pointer-events-none"
40
+ style={style}
41
+ {...props}
42
+ >
43
+ <Animated.View
44
+ style={[
45
+ {
46
+ position: 'absolute',
47
+ top: '-50%',
48
+ left: '-50%',
49
+ width: '200%',
50
+ height: '200%',
51
+ },
52
+ animatedStyle,
53
+ ]}
54
+ >
55
+ {/* The beam is a conic/linear sweep. Linear works reasonably well when masked by borders. */}
56
+ <LinearGradient
57
+ colors={[color, 'transparent', 'transparent']}
58
+ start={{ x: 0.5, y: 0.5 }}
59
+ end={{ x: 1, y: 1 }}
60
+ style={StyleSheet.absoluteFill}
61
+ />
62
+ </Animated.View>
63
+ <View className="absolute inset-[2px] rounded-[inherit] bg-background" />
64
+ </View>
65
+ );
66
+ }
67
+ );
68
+ BorderBeam.displayName = 'BorderBeam';
@@ -0,0 +1,88 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View, StyleSheet, Dimensions } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withTiming,
7
+ withDelay,
8
+ Easing,
9
+ runOnJS,
10
+ } from 'react-native-reanimated';
11
+
12
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
13
+ const COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7'];
14
+
15
+ export const Confetti = ({ count = 50, duration = 3000, onComplete }: { count?: number; duration?: number; onComplete?: () => void }) => {
16
+ const [pieces, setPieces] = useState<any[]>([]);
17
+
18
+ useEffect(() => {
19
+ const newPieces = Array.from({ length: count }).map((_, i) => ({
20
+ id: i,
21
+ x: Math.random() * SCREEN_WIDTH,
22
+ color: COLORS[Math.floor(Math.random() * COLORS.length)],
23
+ delay: Math.random() * 500,
24
+ size: Math.random() * 10 + 5,
25
+ }));
26
+ setPieces(newPieces);
27
+ }, [count]);
28
+
29
+ return (
30
+ <View style={StyleSheet.absoluteFill} pointerEvents="none">
31
+ {pieces.map((p, i) => (
32
+ <ConfettiPiece
33
+ key={p.id}
34
+ x={p.x}
35
+ color={p.color}
36
+ delay={p.delay}
37
+ size={p.size}
38
+ duration={duration}
39
+ onComplete={i === pieces.length - 1 ? onComplete : undefined}
40
+ />
41
+ ))}
42
+ </View>
43
+ );
44
+ };
45
+
46
+ const ConfettiPiece = ({ x, color, delay, size, duration, onComplete }: any) => {
47
+ const translateY = useSharedValue(-50);
48
+ const rotateX = useSharedValue(0);
49
+ const rotateY = useSharedValue(0);
50
+
51
+ useEffect(() => {
52
+ translateY.value = withDelay(
53
+ delay,
54
+ withTiming(SCREEN_HEIGHT + 50, { duration, easing: Easing.linear }, (finished) => {
55
+ if (finished && onComplete) {
56
+ runOnJS(onComplete)();
57
+ }
58
+ })
59
+ );
60
+
61
+ rotateX.value = withDelay(delay, withTiming(Math.random() * 720, { duration, easing: Easing.linear }));
62
+ rotateY.value = withDelay(delay, withTiming(Math.random() * 720, { duration, easing: Easing.linear }));
63
+ }, []);
64
+
65
+ const animatedStyle = useAnimatedStyle(() => ({
66
+ transform: [
67
+ { translateY: translateY.value },
68
+ { rotateX: `${rotateX.value}deg` },
69
+ { rotateY: `${rotateY.value}deg` },
70
+ ],
71
+ }));
72
+
73
+ return (
74
+ <Animated.View
75
+ style={[
76
+ {
77
+ position: 'absolute',
78
+ top: 0,
79
+ left: x,
80
+ width: size,
81
+ height: size,
82
+ backgroundColor: color,
83
+ },
84
+ animatedStyle,
85
+ ]}
86
+ />
87
+ );
88
+ };
@@ -0,0 +1,65 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, StyleSheet, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ interpolateColor,
10
+ } from 'react-native-reanimated';
11
+ import { LinearGradient } from 'expo-linear-gradient';
12
+ import { cn } from '../../lib/utils';
13
+
14
+ export interface MagicCardProps extends ViewProps {
15
+ children: React.ReactNode;
16
+ }
17
+
18
+ export const MagicCard = React.forwardRef<React.ElementRef<typeof View>, MagicCardProps>(
19
+ ({ className, children, style, ...props }, ref) => {
20
+ const rotation = useSharedValue(0);
21
+
22
+ useEffect(() => {
23
+ rotation.value = withRepeat(
24
+ withTiming(360, { duration: 4000, easing: Easing.linear }),
25
+ -1,
26
+ false
27
+ );
28
+ }, []);
29
+
30
+ const animatedStyle = useAnimatedStyle(() => ({
31
+ transform: [{ rotate: `${rotation.value}deg` }],
32
+ }));
33
+
34
+ return (
35
+ <View
36
+ ref={ref}
37
+ className={cn('relative overflow-hidden rounded-2xl bg-card p-[1px]', className)}
38
+ style={style}
39
+ {...props}
40
+ >
41
+ {/* The rotating gradient background behind the inner card */}
42
+ <Animated.View
43
+ style={[
44
+ StyleSheet.absoluteFill,
45
+ { width: '200%', height: '200%', top: '-50%', left: '-50%' },
46
+ animatedStyle,
47
+ ]}
48
+ >
49
+ <LinearGradient
50
+ colors={['transparent', 'hsl(var(--primary))', 'transparent']}
51
+ start={{ x: 0, y: 0 }}
52
+ end={{ x: 1, y: 1 }}
53
+ style={StyleSheet.absoluteFill}
54
+ />
55
+ </Animated.View>
56
+
57
+ {/* Inner Card content */}
58
+ <View className="relative h-full w-full rounded-[15px] bg-card p-6">
59
+ {children}
60
+ </View>
61
+ </View>
62
+ );
63
+ }
64
+ );
65
+ MagicCard.displayName = 'MagicCard';
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, type ViewProps, Dimensions } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ interpolate,
10
+ } from 'react-native-reanimated';
11
+ import { LinearGradient } from 'expo-linear-gradient';
12
+ import { cn } from '../../lib/utils';
13
+
14
+ export interface MeteorsProps extends ViewProps {
15
+ number?: number;
16
+ }
17
+
18
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
19
+
20
+ export const Meteors = React.forwardRef<React.ElementRef<typeof View>, MeteorsProps>(
21
+ ({ className, number = 20, style, ...props }, ref) => {
22
+
23
+ return (
24
+ <View
25
+ ref={ref}
26
+ className={cn('absolute inset-0 overflow-hidden', className)}
27
+ style={style}
28
+ pointerEvents="none"
29
+ {...props}
30
+ >
31
+ {Array.from({ length: number }).map((_, idx) => (
32
+ <Meteor key={idx} />
33
+ ))}
34
+ </View>
35
+ );
36
+ }
37
+ );
38
+ Meteors.displayName = 'Meteors';
39
+
40
+ const Meteor = () => {
41
+ const animatedValue = useSharedValue(0);
42
+
43
+ // Randomize initial properties
44
+ const top = Math.random() * SCREEN_HEIGHT * 0.5 - 100;
45
+ const left = Math.random() * SCREEN_WIDTH * 1.5 - SCREEN_WIDTH * 0.2;
46
+ const delay = Math.random() * 5000;
47
+ const duration = Math.random() * 2000 + 1500;
48
+
49
+ React.useEffect(() => {
50
+ setTimeout(() => {
51
+ animatedValue.value = withRepeat(
52
+ withTiming(1, { duration, easing: Easing.linear }),
53
+ -1,
54
+ false
55
+ );
56
+ }, delay);
57
+ }, []);
58
+
59
+ const animatedStyle = useAnimatedStyle(() => {
60
+ const translateX = interpolate(animatedValue.value, [0, 1], [0, -SCREEN_WIDTH * 1.5]);
61
+ const translateY = interpolate(animatedValue.value, [0, 1], [0, SCREEN_WIDTH * 1.5]);
62
+ const opacity = interpolate(animatedValue.value, [0, 0.1, 0.8, 1], [0, 1, 1, 0]);
63
+
64
+ return {
65
+ transform: [
66
+ { translateX },
67
+ { translateY },
68
+ { rotate: '-45deg' },
69
+ ],
70
+ opacity,
71
+ };
72
+ });
73
+
74
+ return (
75
+ <Animated.View
76
+ style={[
77
+ {
78
+ position: 'absolute',
79
+ top,
80
+ left,
81
+ width: 100,
82
+ height: 2,
83
+ },
84
+ animatedStyle,
85
+ ]}
86
+ >
87
+ <LinearGradient
88
+ colors={['rgba(255,255,255,0.8)', 'transparent']}
89
+ start={{ x: 0, y: 0 }}
90
+ end={{ x: 1, y: 0 }}
91
+ style={StyleSheet.absoluteFill}
92
+ />
93
+ </Animated.View>
94
+ );
95
+ };