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,145 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import { View, Pressable, Modal, type ViewProps, type LayoutRectangle } from 'react-native';
3
+ import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } from 'react-native-reanimated';
4
+ import { useControllableState } from '../../hooks/use-controllable';
5
+ import { cn } from '../../lib/utils';
6
+ import { Text } from '../typography';
7
+ import { useHaptics } from '../../hooks/use-haptics';
8
+
9
+ const DropdownMenuContext = React.createContext<{
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ triggerLayout: LayoutRectangle | null;
13
+ setTriggerLayout: (layout: LayoutRectangle | null) => void;
14
+ } | null>(null);
15
+
16
+ export const DropdownMenu = ({ open: openProp, defaultOpen, onOpenChange, children }: any) => {
17
+ const [open, setOpen] = useControllableState({
18
+ prop: openProp,
19
+ defaultProp: defaultOpen || false,
20
+ onChange: onOpenChange,
21
+ });
22
+ const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
23
+
24
+ return (
25
+ <DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerLayout, setTriggerLayout }}>
26
+ {children}
27
+ </DropdownMenuContext.Provider>
28
+ );
29
+ };
30
+
31
+ export const DropdownMenuTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
32
+ ({ children, ...props }, ref) => {
33
+ const context = React.useContext(DropdownMenuContext);
34
+ if (!context) throw new Error('DropdownMenuTrigger must be used within DropdownMenu');
35
+
36
+ return (
37
+ <View
38
+ ref={ref}
39
+ collapsable={false}
40
+ onLayout={(e) => {
41
+ (e.target as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
42
+ context.setTriggerLayout({ x: pageX, y: pageY, width, height });
43
+ });
44
+ }}
45
+ >
46
+ <Pressable onPress={() => context.onOpenChange(true)} {...props}>
47
+ {children}
48
+ </Pressable>
49
+ </View>
50
+ );
51
+ }
52
+ );
53
+ DropdownMenuTrigger.displayName = 'DropdownMenuTrigger';
54
+
55
+ export const DropdownMenuContent = forwardRef<React.ElementRef<typeof View>, ViewProps & { align?: 'start' | 'center' | 'end' }>(
56
+ ({ className, children, align = 'center', ...props }, ref) => {
57
+ const context = React.useContext(DropdownMenuContext);
58
+ if (!context) throw new Error('DropdownMenuContent must be used within DropdownMenu');
59
+
60
+ if (!context.triggerLayout) return null;
61
+
62
+ const { x, y, width, height } = context.triggerLayout;
63
+
64
+ // Very basic positioning relative to the trigger.
65
+ let leftPosition = x;
66
+ if (align === 'center') leftPosition = x + width / 2;
67
+ if (align === 'end') leftPosition = x + width;
68
+
69
+ return (
70
+ <Modal
71
+ visible={context.open}
72
+ transparent
73
+ animationType="none"
74
+ onRequestClose={() => context.onOpenChange(false)}
75
+ >
76
+ <View className="flex-1">
77
+ <Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
78
+ <Animated.View
79
+ entering={ZoomIn.duration(200)}
80
+ exiting={FadeOut.duration(150)}
81
+ style={{
82
+ position: 'absolute',
83
+ top: y + height + 8,
84
+ left: leftPosition,
85
+ transform: align === 'center' ? [{ translateX: '-50%' }] : align === 'end' ? [{ translateX: '-100%' }] : [],
86
+ }}
87
+ className={cn(
88
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
89
+ className
90
+ )}
91
+ {...props}
92
+ >
93
+ {children}
94
+ </Animated.View>
95
+ </View>
96
+ </Modal>
97
+ );
98
+ }
99
+ );
100
+ DropdownMenuContent.displayName = 'DropdownMenuContent';
101
+
102
+ export const DropdownMenuItem = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable> & { inset?: boolean }>(
103
+ ({ className, inset, onPress, children, ...props }, ref) => {
104
+ const context = React.useContext(DropdownMenuContext);
105
+ const triggerHaptic = useHaptics();
106
+
107
+ return (
108
+ <Pressable
109
+ ref={ref}
110
+ onPress={(e) => {
111
+ triggerHaptic('selection');
112
+ onPress?.(e);
113
+ context?.onOpenChange(false);
114
+ }}
115
+ className={cn(
116
+ 'relative flex flex-row cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
117
+ inset && 'pl-8',
118
+ className
119
+ )}
120
+ {...props}
121
+ >
122
+ <Text className="text-sm font-medium text-popover-foreground">{children as any}</Text>
123
+ </Pressable>
124
+ );
125
+ }
126
+ );
127
+ DropdownMenuItem.displayName = 'DropdownMenuItem';
128
+
129
+ export const DropdownMenuSeparator = forwardRef<React.ElementRef<typeof View>, ViewProps>(
130
+ ({ className, ...props }, ref) => (
131
+ <View ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
132
+ )
133
+ );
134
+ DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
135
+
136
+ export const DropdownMenuLabel = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text> & { inset?: boolean }>(
137
+ ({ className, inset, ...props }, ref) => (
138
+ <Text
139
+ ref={ref}
140
+ className={cn('px-2 py-1.5 text-sm font-semibold text-popover-foreground', inset && 'pl-8', className)}
141
+ {...props}
142
+ />
143
+ )
144
+ );
145
+ DropdownMenuLabel.displayName = 'DropdownMenuLabel';
@@ -0,0 +1 @@
1
+ export * from './dropdown-menu';
@@ -0,0 +1,84 @@
1
+ import React, { forwardRef, createContext, useContext } from 'react';
2
+ import { View, type ViewProps, TextInput, type TextInputProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+ import { Text } from '../typography';
5
+
6
+ const FormItemContext = createContext<{ id: string } | null>(null);
7
+
8
+ export const Form = forwardRef<React.ElementRef<typeof View>, ViewProps>(
9
+ ({ className, ...props }, ref) => (
10
+ <View ref={ref} className={cn('space-y-6', className)} {...props} />
11
+ )
12
+ );
13
+ Form.displayName = 'Form';
14
+
15
+ export const FormItem = forwardRef<React.ElementRef<typeof View>, ViewProps>(
16
+ ({ className, ...props }, ref) => {
17
+ const id = React.useId();
18
+ return (
19
+ <FormItemContext.Provider value={{ id }}>
20
+ <View ref={ref} className={cn('space-y-2', className)} {...props} />
21
+ </FormItemContext.Provider>
22
+ );
23
+ }
24
+ );
25
+ FormItem.displayName = 'FormItem';
26
+
27
+ export const FormLabel = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text> & { error?: boolean }>(
28
+ ({ className, error, ...props }, ref) => {
29
+ return (
30
+ <Text
31
+ ref={ref}
32
+ className={cn(
33
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
34
+ error && 'text-destructive',
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+ );
42
+ FormLabel.displayName = 'FormLabel';
43
+
44
+ export const FormControl = forwardRef<React.ElementRef<typeof View>, ViewProps & { error?: boolean }>(
45
+ ({ className, error, ...props }, ref) => {
46
+ return (
47
+ <View
48
+ ref={ref}
49
+ className={cn('w-full', className)}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+ );
55
+ FormControl.displayName = 'FormControl';
56
+
57
+ export const FormDescription = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
58
+ ({ className, ...props }, ref) => {
59
+ return (
60
+ <Text
61
+ ref={ref}
62
+ className={cn('text-[0.8rem] text-muted-foreground', className)}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+ );
68
+ FormDescription.displayName = 'FormDescription';
69
+
70
+ export const FormMessage = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
71
+ ({ className, children, ...props }, ref) => {
72
+ if (!children) return null;
73
+ return (
74
+ <Text
75
+ ref={ref}
76
+ className={cn('text-[0.8rem] font-medium text-destructive', className)}
77
+ {...props}
78
+ >
79
+ {children}
80
+ </Text>
81
+ );
82
+ }
83
+ );
84
+ FormMessage.displayName = 'FormMessage';
@@ -0,0 +1 @@
1
+ export * from './form';
@@ -0,0 +1 @@
1
+ export * from './input';
@@ -0,0 +1,115 @@
1
+ import React, { useState } from 'react';
2
+ import { TextInput, View, type TextInputProps } from 'react-native';
3
+ import { cva, type VariantProps } from '../../lib/variants';
4
+ import { Text } from '../typography';
5
+ import { useThemeContext } from '../../context/theme-context';
6
+ import Animated, { useAnimatedStyle, withSpring, useSharedValue, interpolateColor } from 'react-native-reanimated';
7
+
8
+ const inputVariants = cva(
9
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: '',
14
+ glass: 'bg-glass border-glass-border',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'default',
19
+ },
20
+ }
21
+ );
22
+
23
+ export interface InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
24
+ label?: string;
25
+ floatingLabel?: boolean;
26
+ error?: string;
27
+ helperText?: string;
28
+ leftIcon?: React.ReactNode;
29
+ rightIcon?: React.ReactNode;
30
+ }
31
+
32
+ const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
33
+
34
+ export const Input = React.forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
35
+ ({ className, variant, label, floatingLabel, error, helperText, leftIcon, rightIcon, onFocus, onBlur, value, ...props }, ref) => {
36
+ const [isFocused, setIsFocused] = useState(false);
37
+ const { theme } = useThemeContext();
38
+
39
+ // Floating label animation state
40
+ const floatingAnim = useSharedValue(value ? 1 : 0);
41
+
42
+ const handleFocus = (e: any) => {
43
+ setIsFocused(true);
44
+ if (floatingLabel) floatingAnim.value = withSpring(1, { damping: 20, stiffness: 300 });
45
+ onFocus?.(e);
46
+ };
47
+
48
+ const handleBlur = (e: any) => {
49
+ setIsFocused(false);
50
+ if (floatingLabel && !value) floatingAnim.value = withSpring(0, { damping: 20, stiffness: 300 });
51
+ onBlur?.(e);
52
+ };
53
+
54
+ const labelStyle = useAnimatedStyle(() => {
55
+ return {
56
+ transform: [
57
+ { translateY: floatingAnim.value === 1 ? -24 : 0 },
58
+ { scale: floatingAnim.value === 1 ? 0.85 : 1 },
59
+ ],
60
+ opacity: floatingAnim.value === 1 ? 1 : 0.7,
61
+ };
62
+ });
63
+
64
+ return (
65
+ <View className="flex flex-col space-y-1.5 w-full relative">
66
+ {label && !floatingLabel && (
67
+ <Text className="text-sm font-medium text-foreground mb-1">{label}</Text>
68
+ )}
69
+
70
+ <View className="relative w-full justify-center">
71
+ {leftIcon && (
72
+ <View className="absolute left-3 z-10 justify-center h-full">
73
+ {leftIcon}
74
+ </View>
75
+ )}
76
+
77
+ {label && floatingLabel && (
78
+ <Animated.Text
79
+ style={[
80
+ labelStyle,
81
+ { position: 'absolute', left: leftIcon ? 36 : 12, top: 10 },
82
+ ]}
83
+ className="text-sm font-medium text-muted-foreground pointer-events-none z-10"
84
+ >
85
+ {label}
86
+ </Animated.Text>
87
+ )}
88
+
89
+ <AnimatedTextInput
90
+ ref={ref as any}
91
+ className={inputVariants({ variant, className: [className, leftIcon && 'pl-10', rightIcon && 'pr-10', error && 'border-destructive focus-visible:ring-destructive'].filter(Boolean).join(' ') })}
92
+ onFocus={handleFocus}
93
+ onBlur={handleBlur}
94
+ value={value}
95
+ placeholderTextColor={theme.muted.foreground}
96
+ {...props}
97
+ />
98
+
99
+ {rightIcon && (
100
+ <View className="absolute right-3 z-10 justify-center h-full">
101
+ {rightIcon}
102
+ </View>
103
+ )}
104
+ </View>
105
+
106
+ {(helperText || error) && (
107
+ <Text className={`text-sm ${error ? 'text-destructive' : 'text-muted-foreground'}`}>
108
+ {error || helperText}
109
+ </Text>
110
+ )}
111
+ </View>
112
+ );
113
+ }
114
+ );
115
+ Input.displayName = 'Input';
@@ -0,0 +1 @@
1
+ export * from './label';
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { createComponent } from '../../lib/create-component';
3
+ import { Text, type TextProps } from '../typography';
4
+
5
+ const labelVariants = 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70';
6
+
7
+ export interface LabelProps extends TextProps {}
8
+
9
+ export const Label = createComponent<React.ElementRef<typeof Text>, LabelProps>({
10
+ Component: Text,
11
+ baseClassName: labelVariants,
12
+ });
13
+ Label.displayName = 'Label';
@@ -0,0 +1 @@
1
+ export * from './navigation-menu';
@@ -0,0 +1,68 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+ import { Text } from '../typography';
5
+
6
+ export const NavigationMenu = forwardRef<React.ElementRef<typeof View>, ViewProps>(
7
+ ({ className, children, ...props }, ref) => (
8
+ <View
9
+ ref={ref}
10
+ className={cn('relative z-10 flex max-w-max flex-1 items-center justify-center', className)}
11
+ {...props}
12
+ >
13
+ {children}
14
+ </View>
15
+ )
16
+ );
17
+ NavigationMenu.displayName = 'NavigationMenu';
18
+
19
+ export const NavigationMenuList = forwardRef<React.ElementRef<typeof View>, ViewProps>(
20
+ ({ className, ...props }, ref) => (
21
+ <View
22
+ ref={ref}
23
+ className={cn('group flex flex-1 flex-row list-none items-center justify-center space-x-1', className)}
24
+ {...props}
25
+ />
26
+ )
27
+ );
28
+ NavigationMenuList.displayName = 'NavigationMenuList';
29
+
30
+ export const NavigationMenuItem = forwardRef<React.ElementRef<typeof View>, ViewProps>(
31
+ ({ className, ...props }, ref) => (
32
+ <View ref={ref} className={className} {...props} />
33
+ )
34
+ );
35
+ NavigationMenuItem.displayName = 'NavigationMenuItem';
36
+
37
+ export const NavigationMenuTrigger = forwardRef<React.ElementRef<typeof Pressable>, PressableProps>(
38
+ ({ className, children, ...props }, ref) => (
39
+ <Pressable
40
+ ref={ref}
41
+ className={cn(
42
+ 'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50',
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ {typeof children === 'string' ? <Text className="font-medium">{children}</Text> : children}
48
+ </Pressable>
49
+ )
50
+ );
51
+ NavigationMenuTrigger.displayName = 'NavigationMenuTrigger';
52
+
53
+ export const NavigationMenuLink = forwardRef<React.ElementRef<typeof Pressable>, PressableProps & { active?: boolean }>(
54
+ ({ className, active, children, ...props }, ref) => (
55
+ <Pressable
56
+ ref={ref}
57
+ className={cn(
58
+ 'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
59
+ active && 'bg-accent/50',
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {typeof children === 'string' ? <Text>{children}</Text> : children}
65
+ </Pressable>
66
+ )
67
+ );
68
+ NavigationMenuLink.displayName = 'NavigationMenuLink';
@@ -0,0 +1 @@
1
+ export * from './pagination';
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react-native';
4
+ import { cn } from '../../lib/utils';
5
+ import { Button, type ButtonProps } from '../button';
6
+ import { Text } from '../typography';
7
+
8
+ export const Pagination = ({ className, ...props }: ViewProps) => (
9
+ <View
10
+ role="navigation"
11
+ aria-label="pagination"
12
+ className={cn('mx-auto flex w-full justify-center flex-row', className)}
13
+ {...props}
14
+ />
15
+ );
16
+ Pagination.displayName = 'Pagination';
17
+
18
+ export const PaginationContent = React.forwardRef<React.ElementRef<typeof View>, ViewProps>(
19
+ ({ className, ...props }, ref) => (
20
+ <View ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
21
+ )
22
+ );
23
+ PaginationContent.displayName = 'PaginationContent';
24
+
25
+ export const PaginationItem = React.forwardRef<React.ElementRef<typeof View>, ViewProps>(
26
+ ({ className, ...props }, ref) => <View ref={ref} className={cn('', className)} {...props} />
27
+ );
28
+ PaginationItem.displayName = 'PaginationItem';
29
+
30
+ type PaginationLinkProps = {
31
+ isActive?: boolean;
32
+ } & ButtonProps;
33
+
34
+ export const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
35
+ <Button
36
+ aria-current={isActive ? 'page' : undefined}
37
+ variant={isActive ? 'outline' : 'ghost'}
38
+ size={size}
39
+ className={cn('w-9 h-9', className)}
40
+ {...props}
41
+ />
42
+ );
43
+ PaginationLink.displayName = 'PaginationLink';
44
+
45
+ export const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
46
+ <PaginationLink aria-label="Go to previous page" size="default" className={cn('gap-1 pl-2.5', className)} {...props}>
47
+ <View className="flex flex-row items-center gap-1">
48
+ <ChevronLeft size={16} className="text-foreground" />
49
+ <Text className="text-sm font-medium">Previous</Text>
50
+ </View>
51
+ </PaginationLink>
52
+ );
53
+ PaginationPrevious.displayName = 'PaginationPrevious';
54
+
55
+ export const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
56
+ <PaginationLink aria-label="Go to next page" size="default" className={cn('gap-1 pr-2.5', className)} {...props}>
57
+ <View className="flex flex-row items-center gap-1">
58
+ <Text className="text-sm font-medium">Next</Text>
59
+ <ChevronRight size={16} className="text-foreground" />
60
+ </View>
61
+ </PaginationLink>
62
+ );
63
+ PaginationNext.displayName = 'PaginationNext';
64
+
65
+ export const PaginationEllipsis = ({ className, ...props }: ViewProps) => (
66
+ <View aria-hidden className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}>
67
+ <MoreHorizontal size={16} className="text-foreground" />
68
+ </View>
69
+ );
70
+ PaginationEllipsis.displayName = 'PaginationEllipsis';
@@ -0,0 +1 @@
1
+ export * from './progress';
@@ -0,0 +1,66 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, withSpring, useSharedValue } from 'react-native-reanimated';
4
+ import { useSpring } from '../../hooks/use-spring';
5
+ import { cn } from '../../lib/utils';
6
+ import { useThemeContext } from '../../context/theme-context';
7
+
8
+ export interface ProgressProps extends ViewProps {
9
+ /** Value between 0 and 100 */
10
+ value?: number;
11
+ /** Max value, defaults to 100 */
12
+ max?: number;
13
+ /** If true, the progress bar will animate continuously */
14
+ indeterminate?: boolean;
15
+ }
16
+
17
+ export const Progress = React.forwardRef<React.ElementRef<typeof View>, ProgressProps>(
18
+ ({ className, value = 0, max = 100, indeterminate = false, style, ...props }, ref) => {
19
+ const springConfig = useSpring('gentle');
20
+ const { theme } = useThemeContext();
21
+ const progressWidth = useSharedValue(0);
22
+ const translateX = useSharedValue(-100);
23
+
24
+ // Calculate percentage (0-100)
25
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
26
+
27
+ useEffect(() => {
28
+ if (!indeterminate) {
29
+ progressWidth.value = withSpring(percentage, springConfig);
30
+ }
31
+ }, [percentage, indeterminate, springConfig]);
32
+
33
+ const indicatorStyle = useAnimatedStyle(() => {
34
+ if (indeterminate) {
35
+ // We'll handle indeterminate animation via CSS/Tailwind if possible
36
+ // but for Native, it's better to do a repeating animation loop.
37
+ // For simplicity right now, we'll let it fill or just do a standard fill.
38
+ return { width: '50%' };
39
+ }
40
+ return { width: `${progressWidth.value}%` };
41
+ });
42
+
43
+ return (
44
+ <View
45
+ ref={ref}
46
+ className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}
47
+ style={style}
48
+ accessibilityRole="progressbar"
49
+ accessibilityValue={{ min: 0, max, now: value }}
50
+ {...props}
51
+ >
52
+ <Animated.View
53
+ className={cn(
54
+ 'h-full w-full flex-1 bg-primary transition-all',
55
+ indeterminate && 'animate-pulse' // Nativewind supports animate-pulse out of the box
56
+ )}
57
+ style={[
58
+ indicatorStyle,
59
+ { backgroundColor: theme.primary.DEFAULT as string }
60
+ ]}
61
+ />
62
+ </View>
63
+ );
64
+ }
65
+ );
66
+ Progress.displayName = 'Progress';
@@ -0,0 +1 @@
1
+ export * from './radio-group';
@@ -0,0 +1,90 @@
1
+ import React, { forwardRef, createContext, useContext } from 'react';
2
+ import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated';
4
+ import { useControllableState } from '../../hooks/use-controllable';
5
+ import { useHaptics } from '../../hooks/use-haptics';
6
+ import { useSpring } from '../../hooks/use-spring';
7
+ import { cn } from '../../lib/utils';
8
+ import { Text } from '../typography';
9
+
10
+ const RadioGroupContext = createContext<{
11
+ value: string;
12
+ onValueChange: (value: string) => void;
13
+ } | null>(null);
14
+
15
+ export interface RadioGroupProps extends ViewProps {
16
+ value?: string;
17
+ defaultValue?: string;
18
+ onValueChange?: (value: string) => void;
19
+ }
20
+
21
+ export const RadioGroup = forwardRef<React.ElementRef<typeof View>, RadioGroupProps>(
22
+ ({ className, value: valueProp, defaultValue, onValueChange, children, ...props }, ref) => {
23
+ const [value, setValue] = useControllableState({
24
+ prop: valueProp,
25
+ defaultProp: defaultValue || '',
26
+ onChange: onValueChange,
27
+ });
28
+
29
+ return (
30
+ <RadioGroupContext.Provider value={{ value, onValueChange: setValue }}>
31
+ <View ref={ref} className={cn('flex flex-col gap-2', className)} {...props}>
32
+ {children}
33
+ </View>
34
+ </RadioGroupContext.Provider>
35
+ );
36
+ }
37
+ );
38
+ RadioGroup.displayName = 'RadioGroup';
39
+
40
+ export interface RadioGroupItemProps extends PressableProps {
41
+ value: string;
42
+ label?: string;
43
+ }
44
+
45
+ const AnimatedView = Animated.createAnimatedComponent(View);
46
+
47
+ export const RadioGroupItem = forwardRef<React.ElementRef<typeof Pressable>, RadioGroupItemProps>(
48
+ ({ className, value, label, disabled, style, ...props }, ref) => {
49
+ const context = useContext(RadioGroupContext);
50
+ if (!context) throw new Error('RadioGroupItem must be used within a RadioGroup');
51
+
52
+ const triggerHaptic = useHaptics();
53
+ const springConfig = useSpring('snappy');
54
+ const checked = context.value === value;
55
+
56
+ const handlePress = () => {
57
+ if (disabled) return;
58
+ if (!checked) {
59
+ triggerHaptic('selection');
60
+ context.onValueChange(value);
61
+ }
62
+ };
63
+
64
+ const indicatorStyle = useAnimatedStyle(() => {
65
+ const scale = withSpring(checked ? 1 : 0, springConfig);
66
+ return { transform: [{ scale }] };
67
+ }, [checked, springConfig]);
68
+
69
+ return (
70
+ <Pressable
71
+ ref={ref}
72
+ onPress={handlePress}
73
+ disabled={disabled}
74
+ accessibilityRole="radio"
75
+ accessibilityState={{ checked, disabled: !!disabled }}
76
+ className={cn('flex flex-row items-center space-x-2', disabled && 'opacity-50', className)}
77
+ {...props}
78
+ >
79
+ <View className="aspect-square h-5 w-5 items-center justify-center rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
80
+ <AnimatedView
81
+ style={indicatorStyle}
82
+ className="h-2.5 w-2.5 rounded-full bg-primary"
83
+ />
84
+ </View>
85
+ {label && <Text className="text-sm font-medium leading-none">{label}</Text>}
86
+ </Pressable>
87
+ );
88
+ }
89
+ );
90
+ RadioGroupItem.displayName = 'RadioGroupItem';