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,51 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, type ViewProps } from 'react-native';
3
+ import { BlurView, type BlurViewProps } from 'expo-blur';
4
+ import { cva, type VariantProps } from '../../lib/variants';
5
+ import { useThemeContext } from '../../context/theme-context';
6
+ import { cn } from '../../lib/utils';
7
+
8
+ const glassVariants = cva(
9
+ 'overflow-hidden rounded-xl border',
10
+ {
11
+ variants: {
12
+ intensity: {
13
+ light: 'border-white/20 bg-white/10',
14
+ medium: 'border-white/30 bg-white/20',
15
+ heavy: 'border-white/40 bg-white/30',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ intensity: 'medium',
20
+ },
21
+ }
22
+ );
23
+
24
+ export interface GlassCardProps extends Omit<BlurViewProps, 'intensity'>, VariantProps<typeof glassVariants> {
25
+ children?: React.ReactNode;
26
+ }
27
+
28
+ export const GlassCard = React.forwardRef<React.ElementRef<typeof BlurView>, GlassCardProps>(
29
+ ({ className, intensity, tint = 'default', children, style, ...props }, ref) => {
30
+ const { theme, isDark } = useThemeContext();
31
+
32
+ // In dark mode, we generally want a dark tint unless specified
33
+ const resolvedTint = tint === 'default' ? (isDark ? 'dark' : 'light') : tint;
34
+
35
+ return (
36
+ <BlurView
37
+ ref={ref}
38
+ intensity={intensity === 'light' ? 20 : intensity === 'medium' ? 50 : 80}
39
+ tint={resolvedTint}
40
+ className={cn(glassVariants({ intensity, className }))}
41
+ style={[style]}
42
+ {...props}
43
+ >
44
+ <View className="flex-1 p-6">
45
+ {children}
46
+ </View>
47
+ </BlurView>
48
+ );
49
+ }
50
+ );
51
+ GlassCard.displayName = 'GlassCard';
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, type ViewProps } from 'react-native';
3
+ import { BlurView } from 'expo-blur';
4
+ import { useThemeContext } from '../../context/theme-context';
5
+ import { cn } from '../../lib/utils';
6
+ import Animated, { useAnimatedStyle, interpolate, Extrapolate, type SharedValue } from 'react-native-reanimated';
7
+
8
+ export interface GlassHeaderProps extends ViewProps {
9
+ scrollY?: SharedValue<number>;
10
+ collapsedHeight?: number;
11
+ }
12
+
13
+ const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
14
+
15
+ export const GlassHeader = React.forwardRef<React.ElementRef<typeof View>, GlassHeaderProps>(
16
+ ({ className, children, scrollY, collapsedHeight = 100, style, ...props }, ref) => {
17
+ const { isDark } = useThemeContext();
18
+
19
+ const animatedStyle = useAnimatedStyle(() => {
20
+ if (!scrollY) return { opacity: 1 };
21
+
22
+ const opacity = interpolate(
23
+ scrollY.value,
24
+ [0, collapsedHeight / 2, collapsedHeight],
25
+ [0, 0.5, 1],
26
+ Extrapolate.CLAMP
27
+ );
28
+
29
+ return { opacity };
30
+ });
31
+
32
+ return (
33
+ <View
34
+ ref={ref}
35
+ className={cn('absolute top-0 w-full z-50 overflow-hidden', className)}
36
+ style={[{ height: collapsedHeight }, style]}
37
+ {...props}
38
+ >
39
+ {scrollY ? (
40
+ <AnimatedBlurView
41
+ intensity={80}
42
+ tint={isDark ? 'dark' : 'light'}
43
+ style={[StyleSheet.absoluteFill, animatedStyle]}
44
+ />
45
+ ) : (
46
+ <BlurView
47
+ intensity={80}
48
+ tint={isDark ? 'dark' : 'light'}
49
+ style={StyleSheet.absoluteFill}
50
+ />
51
+ )}
52
+
53
+ {/* Safe Area Padding / Content */}
54
+ <View className="flex-1 justify-end pb-4 px-4">
55
+ {children}
56
+ </View>
57
+ </View>
58
+ );
59
+ }
60
+ );
61
+ GlassHeader.displayName = 'GlassHeader';
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, type ViewProps } from 'react-native';
3
+ import { BlurView, type BlurViewProps } from 'expo-blur';
4
+ import { useThemeContext } from '../../context/theme-context';
5
+ import { cn } from '../../lib/utils';
6
+
7
+ export interface GlassPanelProps extends BlurViewProps {
8
+ children?: React.ReactNode;
9
+ }
10
+
11
+ export const GlassPanel = React.forwardRef<React.ElementRef<typeof BlurView>, GlassPanelProps>(
12
+ ({ className, tint = 'default', intensity = 60, children, style, ...props }, ref) => {
13
+ const { isDark } = useThemeContext();
14
+
15
+ const resolvedTint = tint === 'default' ? (isDark ? 'dark' : 'light') : tint;
16
+
17
+ return (
18
+ <View className={cn('overflow-hidden rounded-2xl border border-white/20 shadow-xl', className)} style={style}>
19
+ <BlurView
20
+ ref={ref}
21
+ intensity={intensity}
22
+ tint={resolvedTint}
23
+ className="flex-1"
24
+ {...props}
25
+ >
26
+ {children}
27
+ </BlurView>
28
+ </View>
29
+ );
30
+ }
31
+ );
32
+ GlassPanel.displayName = 'GlassPanel';
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, type ViewProps, Modal, Pressable } from 'react-native';
3
+ import { BlurView } from 'expo-blur';
4
+ import Animated, { SlideInLeft, SlideOutLeft, SlideInRight, SlideOutRight } from 'react-native-reanimated';
5
+ import { cn } from '../../lib/utils';
6
+ import { useThemeContext } from '../../context/theme-context';
7
+
8
+ export interface GlassSidebarProps extends ViewProps {
9
+ open: boolean;
10
+ onOpenChange: (open: boolean) => void;
11
+ children: React.ReactNode;
12
+ side?: 'left' | 'right';
13
+ }
14
+
15
+ export const GlassSidebar = React.forwardRef<React.ElementRef<typeof View>, GlassSidebarProps>(
16
+ ({ className, open, onOpenChange, children, side = 'left', ...props }, ref) => {
17
+ const { isDark } = useThemeContext();
18
+
19
+ const entering = side === 'left' ? SlideInLeft : SlideInRight;
20
+ const exiting = side === 'left' ? SlideOutLeft : SlideOutRight;
21
+
22
+ return (
23
+ <Modal
24
+ visible={open}
25
+ transparent
26
+ animationType="none"
27
+ onRequestClose={() => onOpenChange(false)}
28
+ >
29
+ <View className={cn('flex-1', side === 'left' ? 'justify-start items-start' : 'justify-end items-end')}>
30
+ <Pressable className="absolute inset-0 bg-black/40" onPress={() => onOpenChange(false)} />
31
+
32
+ <Animated.View
33
+ entering={entering.springify().damping(20).stiffness(200)}
34
+ exiting={exiting.duration(300)}
35
+ ref={ref}
36
+ className={cn('h-full w-[80%] max-w-sm overflow-hidden border-white/20 shadow-2xl',
37
+ side === 'left' ? 'rounded-r-3xl border-r' : 'rounded-l-3xl border-l',
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ <BlurView
43
+ intensity={80}
44
+ tint={isDark ? 'dark' : 'light'}
45
+ style={StyleSheet.absoluteFill}
46
+ />
47
+ <View className="flex-1 p-6 pt-safe">
48
+ {children}
49
+ </View>
50
+ </Animated.View>
51
+ </View>
52
+ </Modal>
53
+ );
54
+ }
55
+ );
56
+ GlassSidebar.displayName = 'GlassSidebar';
@@ -0,0 +1,44 @@
1
+ export * from './glass/glass-card';
2
+ export * from './glass/glass-header';
3
+ export * from './glass/glass-panel';
4
+ export * from './glass/glass-bottom-sheet';
5
+ export * from './mobile/swipe-row';
6
+ export * from './mobile/pull-to-refresh';
7
+ export * from './mobile/fab';
8
+ export * from './mobile/bottom-tab-bar';
9
+ export * from './mobile/notification-badge';
10
+ export * from './mobile/haptic-pressable';
11
+ export * from './mobile/biometric-button';
12
+ export * from './magic/animated-number';
13
+ export * from './magic/bento-grid';
14
+ export * from './motion/marquee';
15
+ export * from './motion/blur-fade';
16
+ export * from './motion/pulsating-button';
17
+ export * from './glass/glass-sidebar';
18
+ export * from './mobile/swipeable-card-stack';
19
+ export * from './magic/shimmer';
20
+ export * from './magic/shiny-button';
21
+ export * from './motion/fade-up';
22
+ export * from './motion/slide-in';
23
+ export * from './motion/stagger-children';
24
+ export * from './inputs/phone-input';
25
+ export * from './inputs/currency-input';
26
+ export * from './motion/typing-text';
27
+ export * from './motion/word-pull-up';
28
+ export * from './mobile/scroll-header';
29
+ export * from './magic/ripple';
30
+ export * from './magic/meteors';
31
+ export * from './magic/magic-card';
32
+ export * from './magic/border-beam';
33
+ export * from './magic/confetti';
34
+ export * from './inputs/rating';
35
+ export * from './inputs/color-picker';
36
+ export * from './ai/chat-bubble';
37
+ export * from './ai/typing-indicator';
38
+ export * from './charts/bar-chart';
39
+ export * from './charts/progress-ring';
40
+ export * from './layout/masonry-grid';
41
+ export * from './layout/carousel';
42
+ export * from './layout/parallax-scroll';
43
+ export * from './layout/floating-dock';
44
+ export * from './onboarding/step-indicator';
@@ -0,0 +1,13 @@
1
+ export * from './glass/glass-sidebar';
2
+ export * from './mobile/swipeable-card-stack';
3
+ export * from './magic/shimmer';
4
+ export * from './magic/shiny-button';
5
+ export * from './motion/fade-up';
6
+ export * from './motion/slide-in';
7
+ export * from './motion/stagger-children';
8
+ export * from './inputs/phone-input';
9
+ export * from './inputs/currency-input';
10
+ export * from './motion/typing-text';
11
+ export * from './motion/word-pull-up';
12
+ export * from './mobile/scroll-header';
13
+ export * from './magic/ripple';
@@ -0,0 +1 @@
1
+ export * from './magic/meteors';
@@ -0,0 +1,92 @@
1
+ import React, { useState } from 'react';
2
+ import { View, Pressable, Modal, StyleSheet, Dimensions, type ViewProps } from 'react-native';
3
+ import Animated, { SlideInDown, SlideOutDown } from 'react-native-reanimated';
4
+ import { cn } from '../../lib/utils';
5
+ import { Text } from '../../components/typography';
6
+ import { useHaptics } from '../../hooks/use-haptics';
7
+ import { BlurView } from 'expo-blur';
8
+
9
+ export interface ColorPickerProps extends ViewProps {
10
+ colors?: string[];
11
+ selectedColor: string;
12
+ onColorChange: (color: string) => void;
13
+ }
14
+
15
+ const DEFAULT_COLORS = [
16
+ '#ef4444', // red
17
+ '#f97316', // orange
18
+ '#eab308', // yellow
19
+ '#22c55e', // green
20
+ '#06b6d4', // cyan
21
+ '#3b82f6', // blue
22
+ '#a855f7', // purple
23
+ '#ec4899', // pink
24
+ '#000000', // black
25
+ '#ffffff', // white
26
+ ];
27
+
28
+ export const ColorPicker = React.forwardRef<React.ElementRef<typeof View>, ColorPickerProps>(
29
+ ({ className, colors = DEFAULT_COLORS, selectedColor, onColorChange, ...props }, ref) => {
30
+ const [open, setOpen] = useState(false);
31
+ const triggerHaptic = useHaptics();
32
+
33
+ return (
34
+ <>
35
+ <Pressable
36
+ ref={ref as any}
37
+ onPress={() => setOpen(true)}
38
+ className={cn('flex h-10 w-full flex-row items-center justify-between rounded-md border border-input bg-background px-3 py-2', className)}
39
+ {...props}
40
+ >
41
+ <Text className="text-sm">Choose Color</Text>
42
+ <View
43
+ className="h-6 w-6 rounded-full border border-border"
44
+ style={{ backgroundColor: selectedColor }}
45
+ />
46
+ </Pressable>
47
+
48
+ <Modal visible={open} transparent animationType="none" onRequestClose={() => setOpen(false)}>
49
+ <View className="flex-1 justify-end">
50
+ <Pressable className="absolute inset-0 bg-black/40" onPress={() => setOpen(false)} />
51
+
52
+ <Animated.View
53
+ entering={SlideInDown.springify().damping(20)}
54
+ exiting={SlideOutDown}
55
+ className="bg-background rounded-t-3xl border-t border-border overflow-hidden"
56
+ >
57
+ <BlurView intensity={80} tint="light" style={StyleSheet.absoluteFill} />
58
+ <View className="p-6 pb-safe items-center">
59
+ <View className="w-12 h-1.5 bg-muted rounded-full mb-6" />
60
+ <Text className="text-lg font-semibold mb-4">Select a Color</Text>
61
+
62
+ <View className="flex-row flex-wrap justify-center gap-4">
63
+ {colors.map((c) => (
64
+ <Pressable
65
+ key={c}
66
+ onPress={() => {
67
+ triggerHaptic('selection');
68
+ onColorChange(c);
69
+ setOpen(false);
70
+ }}
71
+ className={cn(
72
+ 'h-12 w-12 rounded-full border shadow-sm items-center justify-center',
73
+ c === '#ffffff' ? 'border-border' : 'border-transparent',
74
+ selectedColor === c && 'ring-2 ring-primary ring-offset-2'
75
+ )}
76
+ style={{ backgroundColor: c }}
77
+ >
78
+ {selectedColor === c && (
79
+ <View className={cn('h-4 w-4 rounded-full', c === '#ffffff' ? 'bg-black' : 'bg-white')} />
80
+ )}
81
+ </Pressable>
82
+ ))}
83
+ </View>
84
+ </View>
85
+ </Animated.View>
86
+ </View>
87
+ </Modal>
88
+ </>
89
+ );
90
+ }
91
+ );
92
+ ColorPicker.displayName = 'ColorPicker';
@@ -0,0 +1,50 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { View, TextInput, type TextInputProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+ import { Text } from '../../components/typography';
5
+
6
+ export interface CurrencyInputProps extends TextInputProps {
7
+ value: string;
8
+ onChangeText: (text: string) => void;
9
+ currencySymbol?: string;
10
+ }
11
+
12
+ export const CurrencyInput = forwardRef<React.ElementRef<typeof TextInput>, CurrencyInputProps>(
13
+ ({ className, value, onChangeText, currencySymbol = '$', ...props }, ref) => {
14
+
15
+ const handleChangeText = (text: string) => {
16
+ // Basic formatting, remove non-numeric except decimal
17
+ let cleanText = text.replace(/[^0-9.]/g, '');
18
+
19
+ // Ensure only one decimal point
20
+ const parts = cleanText.split('.');
21
+ if (parts.length > 2) {
22
+ cleanText = parts[0] + '.' + parts.slice(1).join('');
23
+ }
24
+
25
+ // Limit to 2 decimal places
26
+ if (cleanText.includes('.')) {
27
+ const [integer, decimal = ''] = cleanText.split('.');
28
+ cleanText = `${integer}.${decimal.substring(0, 2)}`;
29
+ }
30
+
31
+ onChangeText(cleanText);
32
+ };
33
+
34
+ return (
35
+ <View className={cn('relative flex h-10 w-full flex-row items-center rounded-md border border-input bg-background px-3', className)}>
36
+ <Text className="text-muted-foreground font-medium mr-1">{currencySymbol}</Text>
37
+ <TextInput
38
+ ref={ref}
39
+ value={value}
40
+ onChangeText={handleChangeText}
41
+ keyboardType="decimal-pad"
42
+ className="flex-1 py-2 text-sm text-foreground focus:outline-none font-medium"
43
+ placeholderTextColor="hsl(var(--muted-foreground))"
44
+ {...props}
45
+ />
46
+ </View>
47
+ );
48
+ }
49
+ );
50
+ CurrencyInput.displayName = 'CurrencyInput';
@@ -0,0 +1,92 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { View, TextInput, Pressable, type ViewProps, type TextInputProps, Keyboard } from 'react-native';
3
+ import Animated, { useAnimatedStyle, withSpring, useSharedValue, withSequence, withTiming } from 'react-native-reanimated';
4
+ import { useHaptics } from '../../hooks/use-haptics';
5
+ import { cn } from '../../lib/utils';
6
+ import { Text } from '../../components/typography';
7
+
8
+ export interface OtpInputProps extends ViewProps {
9
+ length?: number;
10
+ value: string;
11
+ onChangeText: (value: string) => void;
12
+ onComplete?: (value: string) => void;
13
+ }
14
+
15
+ export const OtpInput = React.forwardRef<React.ElementRef<typeof View>, OtpInputProps>(
16
+ ({ className, length = 4, value, onChangeText, onComplete, ...props }, ref) => {
17
+ const [isFocused, setIsFocused] = useState(false);
18
+ const inputRef = useRef<TextInput>(null);
19
+ const triggerHaptic = useHaptics();
20
+
21
+ // Shake animation for error state (could be exposed via props)
22
+ const shakeOffset = useSharedValue(0);
23
+
24
+ const handlePress = () => {
25
+ inputRef.current?.focus();
26
+ };
27
+
28
+ const handleChange = (text: string) => {
29
+ // Keep only numeric (or alphanumeric depending on use case)
30
+ const cleanText = text.replace(/[^0-9]/g, '').slice(0, length);
31
+ if (cleanText !== value) {
32
+ triggerHaptic('light');
33
+ onChangeText(cleanText);
34
+ if (cleanText.length === length) {
35
+ triggerHaptic('success');
36
+ Keyboard.dismiss();
37
+ onComplete?.(cleanText);
38
+ }
39
+ }
40
+ };
41
+
42
+ const shakeStyle = useAnimatedStyle(() => ({
43
+ transform: [{ translateX: shakeOffset.value }],
44
+ }));
45
+
46
+ return (
47
+ <View ref={ref} className={cn('w-full', className)} {...props}>
48
+ <Pressable onPress={handlePress} className="flex-row items-center justify-between gap-2">
49
+ {Array.from({ length }).map((_, index) => {
50
+ const char = value[index] || '';
51
+ const isActive = isFocused && value.length === index;
52
+ const isFilled = value.length > index;
53
+
54
+ return (
55
+ <Animated.View
56
+ key={index}
57
+ style={shakeStyle}
58
+ className={cn(
59
+ 'flex h-14 w-12 items-center justify-center rounded-md border text-center transition-all',
60
+ isActive ? 'border-primary ring-2 ring-primary/20' : 'border-input',
61
+ isFilled ? 'bg-accent/50' : 'bg-background'
62
+ )}
63
+ >
64
+ <Text className="text-xl font-semibold">
65
+ {char}
66
+ </Text>
67
+ {isActive && (
68
+ <View className="absolute bottom-2 h-[2px] w-4 bg-primary animate-pulse" />
69
+ )}
70
+ </Animated.View>
71
+ );
72
+ })}
73
+ </Pressable>
74
+
75
+ {/* Hidden Input to capture native keyboard events easily */}
76
+ <TextInput
77
+ ref={inputRef}
78
+ value={value}
79
+ onChangeText={handleChange}
80
+ maxLength={length}
81
+ keyboardType="number-pad"
82
+ textContentType="oneTimeCode"
83
+ autoComplete="one-time-code"
84
+ className="absolute opacity-0 w-0 h-0"
85
+ onFocus={() => setIsFocused(true)}
86
+ onBlur={() => setIsFocused(false)}
87
+ />
88
+ </View>
89
+ );
90
+ }
91
+ );
92
+ OtpInput.displayName = 'OtpInput';
@@ -0,0 +1,58 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import { View, TextInput, type TextInputProps, Pressable } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+ import { Text } from '../../components/typography';
5
+ import { ChevronDown } from 'lucide-react-native';
6
+ import { useHaptics } from '../../hooks/use-haptics';
7
+
8
+ export interface PhoneInputProps extends TextInputProps {
9
+ value: string;
10
+ onChangeText: (text: string) => void;
11
+ defaultCountryCode?: string;
12
+ onCountryCodePress?: () => void;
13
+ }
14
+
15
+ export const PhoneInput = forwardRef<React.ElementRef<typeof TextInput>, PhoneInputProps>(
16
+ ({ className, value, onChangeText, defaultCountryCode = '+1', onCountryCodePress, ...props }, ref) => {
17
+ const triggerHaptic = useHaptics();
18
+
19
+ // Basic formatter for US numbers (xxx) xxx-xxxx
20
+ const formatPhoneNumber = (text: string): string => {
21
+ const cleaned = ('' + text).replace(/\D/g, '');
22
+ const match = cleaned.match(/^(\d{0,3})(\d{0,3})(\d{0,4})$/);
23
+ if (match) {
24
+ return !match[2] ? (match[1] || '') : `(${match[1] || ''}) ${match[2] || ''}${match[3] ? `-${match[3]}` : ''}`;
25
+ }
26
+ return text;
27
+ };
28
+
29
+ const handleChangeText = (text: string) => {
30
+ onChangeText(formatPhoneNumber(text));
31
+ };
32
+
33
+ return (
34
+ <View className={cn('flex h-10 w-full flex-row items-center rounded-md border border-input bg-background overflow-hidden', className)}>
35
+ <Pressable
36
+ onPress={() => {
37
+ triggerHaptic('selection');
38
+ onCountryCodePress?.();
39
+ }}
40
+ className="flex-row items-center justify-center px-3 border-r border-input bg-muted/50 h-full active:bg-muted"
41
+ >
42
+ <Text className="text-sm font-medium mr-1">{defaultCountryCode}</Text>
43
+ <ChevronDown size={14} className="text-muted-foreground" />
44
+ </Pressable>
45
+ <TextInput
46
+ ref={ref}
47
+ value={value}
48
+ onChangeText={handleChangeText}
49
+ keyboardType="phone-pad"
50
+ className="flex-1 px-3 py-2 text-sm text-foreground focus:outline-none"
51
+ placeholderTextColor="hsl(var(--muted-foreground))"
52
+ {...props}
53
+ />
54
+ </View>
55
+ );
56
+ }
57
+ );
58
+ PhoneInput.displayName = 'PhoneInput';
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { View, Pressable, type ViewProps } from 'react-native';
3
+ import { Star } from 'lucide-react-native';
4
+ import { cn } from '../../lib/utils';
5
+ import { useHaptics } from '../../hooks/use-haptics';
6
+
7
+ export interface RatingProps extends ViewProps {
8
+ maxRating?: number;
9
+ rating: number;
10
+ onRatingChange: (rating: number) => void;
11
+ size?: number;
12
+ readonly?: boolean;
13
+ }
14
+
15
+ export const Rating = React.forwardRef<React.ElementRef<typeof View>, RatingProps>(
16
+ ({ className, maxRating = 5, rating, onRatingChange, size = 24, readonly = false, ...props }, ref) => {
17
+ const triggerHaptic = useHaptics();
18
+
19
+ return (
20
+ <View ref={ref} className={cn('flex-row items-center space-x-1', className)} {...props}>
21
+ {Array.from({ length: maxRating }).map((_, index) => {
22
+ const starValue = index + 1;
23
+ const isFilled = starValue <= rating;
24
+
25
+ return (
26
+ <Pressable
27
+ key={index}
28
+ disabled={readonly}
29
+ onPress={() => {
30
+ if (!readonly) {
31
+ triggerHaptic('light');
32
+ onRatingChange(starValue);
33
+ }
34
+ }}
35
+ className="p-1"
36
+ >
37
+ <Star
38
+ size={size}
39
+ className={cn(
40
+ 'transition-colors',
41
+ isFilled ? 'text-amber-400 fill-amber-400' : 'text-muted-foreground'
42
+ )}
43
+ />
44
+ </Pressable>
45
+ );
46
+ })}
47
+ </View>
48
+ );
49
+ }
50
+ );
51
+ Rating.displayName = 'Rating';
@@ -0,0 +1,57 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { View, ScrollView, Dimensions, type ViewProps, type NativeSyntheticEvent, type NativeScrollEvent } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ export interface CarouselProps extends ViewProps {
6
+ data: any[];
7
+ renderItem: (item: any, index: number) => React.ReactNode;
8
+ itemWidth?: number;
9
+ }
10
+
11
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
12
+
13
+ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, CarouselProps>(
14
+ ({ className, data, renderItem, itemWidth = SCREEN_WIDTH * 0.8, ...props }, ref) => {
15
+ const [activeIndex, setActiveIndex] = useState(0);
16
+
17
+ const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
18
+ const offsetX = event.nativeEvent.contentOffset.x;
19
+ const index = Math.round(offsetX / itemWidth);
20
+ setActiveIndex(index);
21
+ };
22
+
23
+ return (
24
+ <View ref={ref} className={cn('w-full', className)} {...props}>
25
+ <ScrollView
26
+ horizontal
27
+ showsHorizontalScrollIndicator={false}
28
+ snapToInterval={itemWidth}
29
+ decelerationRate="fast"
30
+ onScroll={handleScroll}
31
+ scrollEventThrottle={16}
32
+ contentContainerStyle={{ paddingHorizontal: (SCREEN_WIDTH - itemWidth) / 2 }}
33
+ >
34
+ {data.map((item, index) => (
35
+ <View key={index} style={{ width: itemWidth }} className="px-2">
36
+ {renderItem(item, index)}
37
+ </View>
38
+ ))}
39
+ </ScrollView>
40
+
41
+ {/* Pagination Dots */}
42
+ <View className="flex-row justify-center mt-4 space-x-2">
43
+ {data.map((_, i) => (
44
+ <View
45
+ key={i}
46
+ className={cn(
47
+ 'h-2 rounded-full transition-all duration-300',
48
+ i === activeIndex ? 'w-6 bg-primary' : 'w-2 bg-muted-foreground/30'
49
+ )}
50
+ />
51
+ ))}
52
+ </View>
53
+ </View>
54
+ );
55
+ }
56
+ );
57
+ Carousel.displayName = 'Carousel';