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 @@
1
+ export * from './scroll-area';
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { ScrollView, type ScrollViewProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ export interface ScrollAreaProps extends ScrollViewProps {
6
+ orientation?: 'vertical' | 'horizontal';
7
+ }
8
+
9
+ export const ScrollArea = React.forwardRef<React.ElementRef<typeof ScrollView>, ScrollAreaProps>(
10
+ ({ className, orientation = 'vertical', children, ...props }, ref) => {
11
+ const isHorizontal = orientation === 'horizontal';
12
+
13
+ return (
14
+ <ScrollView
15
+ ref={ref}
16
+ horizontal={isHorizontal}
17
+ showsVerticalScrollIndicator={!isHorizontal}
18
+ showsHorizontalScrollIndicator={isHorizontal}
19
+ className={cn('flex shrink-0', className)}
20
+ {...props}
21
+ >
22
+ {children}
23
+ </ScrollView>
24
+ );
25
+ }
26
+ );
27
+ ScrollArea.displayName = 'ScrollArea';
@@ -0,0 +1 @@
1
+ export * from './select';
@@ -0,0 +1,154 @@
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 { ChevronDown, Check } from 'lucide-react-native';
5
+ import { useControllableState } from '../../hooks/use-controllable';
6
+ import { cn } from '../../lib/utils';
7
+ import { Text } from '../typography';
8
+ import { useHaptics } from '../../hooks/use-haptics';
9
+ import { useThemeContext } from '../../context/theme-context';
10
+
11
+ const SelectContext = React.createContext<{
12
+ open: boolean;
13
+ onOpenChange: (open: boolean) => void;
14
+ value: string;
15
+ onValueChange: (value: string) => void;
16
+ triggerLayout: LayoutRectangle | null;
17
+ setTriggerLayout: (layout: LayoutRectangle | null) => void;
18
+ } | null>(null);
19
+
20
+ export const Select = ({ open: openProp, defaultOpen, onOpenChange, value: valueProp, defaultValue, onValueChange, children }: any) => {
21
+ const [open, setOpen] = useControllableState({
22
+ prop: openProp,
23
+ defaultProp: defaultOpen || false,
24
+ onChange: onOpenChange,
25
+ });
26
+
27
+ const [value, setValue] = useControllableState({
28
+ prop: valueProp,
29
+ defaultProp: defaultValue || '',
30
+ onChange: onValueChange,
31
+ });
32
+
33
+ const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
34
+
35
+ return (
36
+ <SelectContext.Provider value={{ open, onOpenChange: setOpen, value, onValueChange: setValue, triggerLayout, setTriggerLayout }}>
37
+ {children}
38
+ </SelectContext.Provider>
39
+ );
40
+ };
41
+
42
+ export const SelectTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
43
+ ({ className, children, ...props }, ref) => {
44
+ const context = React.useContext(SelectContext);
45
+ if (!context) throw new Error('SelectTrigger must be used within Select');
46
+
47
+ return (
48
+ <View
49
+ ref={ref}
50
+ collapsable={false}
51
+ onLayout={(e) => {
52
+ (e.target as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
53
+ context.setTriggerLayout({ x: pageX, y: pageY, width, height });
54
+ });
55
+ }}
56
+ className={cn(
57
+ 'flex h-10 w-full flex-row items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
58
+ className
59
+ )}
60
+ >
61
+ <Pressable className="flex-1 flex-row justify-between items-center" onPress={() => context.onOpenChange(true)} {...props}>
62
+ {children}
63
+ <ChevronDown size={16} className="text-muted-foreground opacity-50" />
64
+ </Pressable>
65
+ </View>
66
+ );
67
+ }
68
+ );
69
+ SelectTrigger.displayName = 'SelectTrigger';
70
+
71
+ export const SelectValue = ({ placeholder }: { placeholder?: string }) => {
72
+ const context = React.useContext(SelectContext);
73
+ return (
74
+ <Text className={cn('text-sm', !context?.value ? 'text-muted-foreground' : 'text-foreground')} numberOfLines={1}>
75
+ {context?.value || placeholder}
76
+ </Text>
77
+ );
78
+ };
79
+
80
+ export const SelectContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
81
+ ({ className, children, ...props }, ref) => {
82
+ const context = React.useContext(SelectContext);
83
+ if (!context) throw new Error('SelectContent must be used within Select');
84
+
85
+ if (!context.triggerLayout) return null;
86
+
87
+ const { x, y, width, height } = context.triggerLayout;
88
+
89
+ return (
90
+ <Modal
91
+ visible={context.open}
92
+ transparent
93
+ animationType="none"
94
+ onRequestClose={() => context.onOpenChange(false)}
95
+ >
96
+ <View className="flex-1">
97
+ <Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
98
+ <Animated.View
99
+ entering={ZoomIn.duration(150)}
100
+ exiting={FadeOut.duration(100)}
101
+ style={{
102
+ position: 'absolute',
103
+ top: y + height + 4,
104
+ left: x,
105
+ width: width,
106
+ }}
107
+ className={cn(
108
+ 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
109
+ className
110
+ )}
111
+ {...props}
112
+ >
113
+ <View className="p-1">{children}</View>
114
+ </Animated.View>
115
+ </View>
116
+ </Modal>
117
+ );
118
+ }
119
+ );
120
+ SelectContent.displayName = 'SelectContent';
121
+
122
+ export const SelectItem = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable> & { value: string; label: string }>(
123
+ ({ className, value, label, onPress, ...props }, ref) => {
124
+ const context = React.useContext(SelectContext);
125
+ const triggerHaptic = useHaptics();
126
+ const { theme } = useThemeContext();
127
+
128
+ const isSelected = context?.value === value;
129
+
130
+ return (
131
+ <Pressable
132
+ ref={ref}
133
+ onPress={(e) => {
134
+ triggerHaptic('selection');
135
+ context?.onValueChange(value);
136
+ context?.onOpenChange(false);
137
+ onPress?.(e);
138
+ }}
139
+ className={cn(
140
+ 'relative flex w-full flex-row cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground',
141
+ isSelected ? 'bg-accent' : '',
142
+ className
143
+ )}
144
+ {...props}
145
+ >
146
+ <View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
147
+ {isSelected && <Check size={16} color={theme.foreground as string} />}
148
+ </View>
149
+ <Text className="text-sm font-medium text-foreground">{label}</Text>
150
+ </Pressable>
151
+ );
152
+ }
153
+ );
154
+ SelectItem.displayName = 'SelectItem';
@@ -0,0 +1 @@
1
+ export * from './separator';
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { cva, type VariantProps } from '../../lib/variants';
4
+ import { createComponent } from '../../lib/create-component';
5
+
6
+ const separatorVariants = cva('shrink-0 bg-border', {
7
+ variants: {
8
+ orientation: {
9
+ horizontal: 'h-[1px] w-full',
10
+ vertical: 'h-full w-[1px]',
11
+ },
12
+ variant: {
13
+ default: 'bg-border',
14
+ gradient: 'bg-transparent', // Handled by Layer 3 Gradient wrapper
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ orientation: 'horizontal',
19
+ variant: 'default',
20
+ },
21
+ });
22
+
23
+ export interface SeparatorProps extends ViewProps, VariantProps<typeof separatorVariants> {}
24
+
25
+ export const Separator = React.forwardRef<React.ElementRef<typeof View>, SeparatorProps>(
26
+ ({ className, orientation, variant, ...props }, ref) => {
27
+ return (
28
+ <View
29
+ ref={ref}
30
+ className={separatorVariants({ orientation, variant, className })}
31
+ role={props.role ?? 'separator'}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+ );
37
+ Separator.displayName = 'Separator';
@@ -0,0 +1 @@
1
+ export * from './sheet';
@@ -0,0 +1,128 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { View, Modal, Pressable, type ViewProps } from 'react-native';
3
+ import Animated, { FadeIn, FadeOut, SlideInDown, SlideOutDown } from 'react-native-reanimated';
4
+ import { X } from 'lucide-react-native';
5
+ import { useControllableState } from '../../hooks/use-controllable';
6
+ import { cn } from '../../lib/utils';
7
+ import { Text } from '../typography';
8
+
9
+ const SheetContext = React.createContext<{
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ } | null>(null);
13
+
14
+ export interface SheetProps {
15
+ open?: boolean;
16
+ defaultOpen?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ children: React.ReactNode;
19
+ }
20
+
21
+ export const Sheet = ({ open: openProp, defaultOpen, onOpenChange, children }: SheetProps) => {
22
+ const [open, setOpen] = useControllableState({
23
+ prop: openProp,
24
+ defaultProp: defaultOpen || false,
25
+ onChange: onOpenChange,
26
+ });
27
+
28
+ return (
29
+ <SheetContext.Provider value={{ open, onOpenChange: setOpen }}>
30
+ {children}
31
+ </SheetContext.Provider>
32
+ );
33
+ };
34
+
35
+ export const SheetTrigger = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable>>(
36
+ ({ children, onPress, ...props }, ref) => {
37
+ const context = React.useContext(SheetContext);
38
+ if (!context) throw new Error('SheetTrigger must be used within Sheet');
39
+
40
+ return (
41
+ <Pressable ref={ref} onPress={(e) => { context.onOpenChange(true); onPress?.(e); }} {...props}>
42
+ {children}
43
+ </Pressable>
44
+ );
45
+ }
46
+ );
47
+ SheetTrigger.displayName = 'SheetTrigger';
48
+
49
+ export interface SheetContentProps extends ViewProps {
50
+ overlayClassName?: string;
51
+ hideClose?: boolean;
52
+ }
53
+
54
+ export const SheetContent = forwardRef<React.ElementRef<typeof View>, SheetContentProps>(
55
+ ({ className, overlayClassName, children, hideClose = false, ...props }, ref) => {
56
+ const context = React.useContext(SheetContext);
57
+ if (!context) throw new Error('SheetContent must be used within Sheet');
58
+
59
+ return (
60
+ <Modal
61
+ visible={context.open}
62
+ transparent
63
+ animationType="none"
64
+ onRequestClose={() => context.onOpenChange(false)}
65
+ >
66
+ <View className={cn('flex-1 justify-end', overlayClassName)}>
67
+ <Animated.View
68
+ entering={FadeIn.duration(300)}
69
+ exiting={FadeOut.duration(300)}
70
+ className="absolute inset-0 bg-black/80"
71
+ >
72
+ <Pressable className="flex-1" onPress={() => context.onOpenChange(false)} />
73
+ </Animated.View>
74
+
75
+ <Animated.View
76
+ entering={SlideInDown.springify().damping(20).stiffness(200)}
77
+ exiting={SlideOutDown.duration(300)}
78
+ ref={ref}
79
+ className={cn(
80
+ 'z-50 w-full gap-4 bg-background p-6 shadow-lg rounded-t-3xl border-t border-border mt-10',
81
+ className
82
+ )}
83
+ {...props}
84
+ >
85
+ {/* Grab bar for aesthetics */}
86
+ <View className="w-10 h-1 bg-muted rounded-full self-center mb-4" />
87
+
88
+ {children}
89
+
90
+ {!hideClose && (
91
+ <Pressable
92
+ onPress={() => context.onOpenChange(false)}
93
+ className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
94
+ >
95
+ <X size={20} className="text-muted-foreground" />
96
+ </Pressable>
97
+ )}
98
+ </Animated.View>
99
+ </View>
100
+ </Modal>
101
+ );
102
+ }
103
+ );
104
+ SheetContent.displayName = 'SheetContent';
105
+
106
+ export const SheetHeader = ({ className, ...props }: ViewProps) => (
107
+ <View className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
108
+ );
109
+ SheetHeader.displayName = 'SheetHeader';
110
+
111
+ export const SheetFooter = ({ className, ...props }: ViewProps) => (
112
+ <View className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-2 mt-auto pb-8', className)} {...props} />
113
+ );
114
+ SheetFooter.displayName = 'SheetFooter';
115
+
116
+ export const SheetTitle = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
117
+ ({ className, ...props }, ref) => (
118
+ <Text ref={ref} className={cn('text-lg font-semibold text-foreground', className)} {...props} />
119
+ )
120
+ );
121
+ SheetTitle.displayName = 'SheetTitle';
122
+
123
+ export const SheetDescription = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
124
+ ({ className, ...props }, ref) => (
125
+ <Text ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
126
+ )
127
+ );
128
+ SheetDescription.displayName = 'SheetDescription';
@@ -0,0 +1 @@
1
+ export * from './skeleton';
@@ -0,0 +1,84 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ interpolate,
9
+ Extrapolate,
10
+ } from 'react-native-reanimated';
11
+ import { cn } from '../../lib/utils';
12
+ import { useThemeContext } from '../../context/theme-context';
13
+
14
+ export interface SkeletonProps extends ViewProps {
15
+ /** Uses true wave-like shimmer animation instead of opacity pulse */
16
+ shimmer?: boolean;
17
+ }
18
+
19
+ export const Skeleton = React.forwardRef<React.ElementRef<typeof View>, SkeletonProps>(
20
+ ({ className, shimmer = true, style, ...props }, ref) => {
21
+ const { theme } = useThemeContext();
22
+ const progress = useSharedValue(0);
23
+
24
+ useEffect(() => {
25
+ progress.value = withRepeat(
26
+ withTiming(1, { duration: 1200 }),
27
+ -1,
28
+ false
29
+ );
30
+ }, []);
31
+
32
+ const animatedStyle = useAnimatedStyle(() => {
33
+ if (!shimmer) {
34
+ // Fallback to simple opacity pulse if shimmer is false
35
+ return {
36
+ opacity: interpolate(progress.value, [0, 0.5, 1], [0.5, 1, 0.5]),
37
+ };
38
+ }
39
+
40
+ // We translate the shimmer layer across the background
41
+ return {};
42
+ });
43
+
44
+ return (
45
+ <View
46
+ ref={ref}
47
+ className={cn('animate-pulse rounded-md bg-muted overflow-hidden relative', className)}
48
+ style={style}
49
+ {...props}
50
+ >
51
+ {shimmer && (
52
+ <Animated.View
53
+ style={[
54
+ {
55
+ position: 'absolute',
56
+ top: 0,
57
+ left: 0,
58
+ right: 0,
59
+ bottom: 0,
60
+ backgroundColor: theme.shimmer,
61
+ width: '200%', // wider to sweep across
62
+ },
63
+ useAnimatedStyle(() => ({
64
+ transform: [
65
+ {
66
+ translateX: interpolate(
67
+ progress.value,
68
+ [0, 1],
69
+ [-200, 200], // Translate across the screen
70
+ Extrapolate.CLAMP
71
+ ),
72
+ },
73
+ ],
74
+ })),
75
+ ]}
76
+ />
77
+ )}
78
+ </View>
79
+ );
80
+ }
81
+ );
82
+ Skeleton.displayName = 'Skeleton';
83
+
84
+ import { StyleSheet } from 'react-native';
@@ -0,0 +1 @@
1
+ export * from './slider';
@@ -0,0 +1,145 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import { View, type ViewProps, PanResponder } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ runOnJS,
8
+ } from 'react-native-reanimated';
9
+ import { useControllableState } from '../../hooks/use-controllable';
10
+ import { useHaptics } from '../../hooks/use-haptics';
11
+ import { useSpring } from '../../hooks/use-spring';
12
+ import { useThemeContext } from '../../context/theme-context';
13
+ import { cn } from '../../lib/utils';
14
+
15
+ export interface SliderProps extends Omit<ViewProps, 'value'> {
16
+ value?: number;
17
+ defaultValue?: number;
18
+ onValueChange?: (value: number) => void;
19
+ min?: number;
20
+ max?: number;
21
+ step?: number;
22
+ disabled?: boolean;
23
+ }
24
+
25
+ export const Slider = forwardRef<React.ElementRef<typeof View>, SliderProps>(
26
+ (
27
+ {
28
+ className,
29
+ value: valueProp,
30
+ defaultValue = 0,
31
+ onValueChange,
32
+ min = 0,
33
+ max = 100,
34
+ step = 1,
35
+ disabled = false,
36
+ style,
37
+ ...props
38
+ },
39
+ ref
40
+ ) => {
41
+ const triggerHaptic = useHaptics();
42
+ const springConfig = useSpring('snappy');
43
+ const { theme } = useThemeContext();
44
+
45
+ const [value, setValue] = useControllableState({
46
+ prop: valueProp,
47
+ defaultProp: defaultValue,
48
+ onChange: onValueChange,
49
+ });
50
+
51
+ const [width, setWidth] = useState(0);
52
+ const isInteracting = useSharedValue(false);
53
+
54
+ // Ensure value is within bounds
55
+ const clampedValue = Math.min(Math.max(value, min), max);
56
+ const percentage = ((clampedValue - min) / (max - min)) * 100;
57
+
58
+ const handleValueChange = (newPercentage: number) => {
59
+ const rawValue = (newPercentage / 100) * (max - min) + min;
60
+ const steppedValue = Math.round(rawValue / step) * step;
61
+ const finalValue = Math.min(Math.max(steppedValue, min), max);
62
+
63
+ if (finalValue !== value) {
64
+ // Trigger haptic if we crossed a step
65
+ if (step > 0 && finalValue % step === 0) {
66
+ triggerHaptic('selection');
67
+ }
68
+ setValue(finalValue);
69
+ }
70
+ };
71
+
72
+ const panResponder = PanResponder.create({
73
+ onStartShouldSetPanResponder: () => !disabled,
74
+ onMoveShouldSetPanResponder: () => !disabled,
75
+ onPanResponderGrant: (evt) => {
76
+ isInteracting.value = true;
77
+ triggerHaptic('light');
78
+ if (width > 0) {
79
+ const newPercentage = (evt.nativeEvent.locationX / width) * 100;
80
+ handleValueChange(newPercentage);
81
+ }
82
+ },
83
+ onPanResponderMove: (evt, gestureState) => {
84
+ if (width > 0) {
85
+ // Adjust based on the initial touch + drag distance
86
+ const newPercentage = ((evt.nativeEvent.locationX) / width) * 100;
87
+ handleValueChange(newPercentage);
88
+ }
89
+ },
90
+ onPanResponderRelease: () => {
91
+ isInteracting.value = false;
92
+ triggerHaptic('light');
93
+ },
94
+ });
95
+
96
+ const trackAnimatedStyle = useAnimatedStyle(() => {
97
+ return {
98
+ width: withSpring(`${percentage}%`, springConfig),
99
+ };
100
+ }, [percentage, springConfig]);
101
+
102
+ const thumbAnimatedStyle = useAnimatedStyle(() => {
103
+ return {
104
+ left: withSpring(`${percentage}%`, springConfig),
105
+ transform: [
106
+ { translateX: -10 }, // Half of thumb width
107
+ { scale: withSpring(isInteracting.value ? 1.2 : 1, springConfig) }
108
+ ]
109
+ };
110
+ }, [percentage, isInteracting.value, springConfig]);
111
+
112
+ return (
113
+ <View
114
+ ref={ref}
115
+ className={cn(
116
+ 'relative flex w-full touch-none select-none items-center justify-center py-4',
117
+ disabled && 'opacity-50',
118
+ className
119
+ )}
120
+ style={style}
121
+ accessibilityRole="adjustable"
122
+ accessibilityValue={{ min, max, now: clampedValue }}
123
+ {...props}
124
+ >
125
+ <View
126
+ className="relative h-2 w-full overflow-hidden rounded-full bg-secondary"
127
+ onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
128
+ {...panResponder.panHandlers}
129
+ >
130
+ <Animated.View
131
+ className="absolute h-full bg-primary"
132
+ style={trackAnimatedStyle}
133
+ />
134
+ </View>
135
+
136
+ <Animated.View
137
+ className="absolute h-5 w-5 rounded-full border-2 border-primary bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
138
+ style={thumbAnimatedStyle}
139
+ pointerEvents="none"
140
+ />
141
+ </View>
142
+ );
143
+ }
144
+ );
145
+ Slider.displayName = 'Slider';
@@ -0,0 +1 @@
1
+ export * from './switch';
@@ -0,0 +1,78 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { Pressable, View, type PressableProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ withSpring,
6
+ interpolateColor,
7
+ } from 'react-native-reanimated';
8
+ import { useControllableState } from '../../hooks/use-controllable';
9
+ import { useHaptics } from '../../hooks/use-haptics';
10
+ import { useSpring } from '../../hooks/use-spring';
11
+ import { useThemeContext } from '../../context/theme-context';
12
+ import { cn } from '../../lib/utils';
13
+
14
+ export interface SwitchProps extends Omit<PressableProps, 'value'> {
15
+ checked?: boolean;
16
+ defaultChecked?: boolean;
17
+ onCheckedChange?: (checked: boolean) => void;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
22
+
23
+ export const Switch = forwardRef<React.ElementRef<typeof Pressable>, SwitchProps>(
24
+ ({ className, checked: checkedProp, defaultChecked, onCheckedChange, disabled, style, ...props }, ref) => {
25
+ const triggerHaptic = useHaptics();
26
+ const springConfig = useSpring('snappy');
27
+ const { theme } = useThemeContext();
28
+
29
+ const [checked, setChecked] = useControllableState({
30
+ prop: checkedProp,
31
+ defaultProp: defaultChecked || false,
32
+ onChange: onCheckedChange,
33
+ });
34
+
35
+ const handlePress = () => {
36
+ if (disabled) return;
37
+ triggerHaptic('light');
38
+ setChecked(!checked);
39
+ };
40
+
41
+ const trackStyle = useAnimatedStyle(() => {
42
+ const backgroundColor = interpolateColor(
43
+ checked ? 1 : 0,
44
+ [0, 1],
45
+ [theme.input, theme.primary.DEFAULT]
46
+ );
47
+ return { backgroundColor };
48
+ }, [checked, theme]);
49
+
50
+ const thumbStyle = useAnimatedStyle(() => {
51
+ const translateX = withSpring(checked ? 20 : 0, springConfig);
52
+ return { transform: [{ translateX }] };
53
+ }, [checked, springConfig]);
54
+
55
+ return (
56
+ <AnimatedPressable
57
+ ref={ref}
58
+ onPress={handlePress}
59
+ disabled={disabled}
60
+ accessibilityRole="switch"
61
+ accessibilityState={{ checked, disabled }}
62
+ className={cn(
63
+ 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
64
+ disabled && 'cursor-not-allowed opacity-50',
65
+ className
66
+ )}
67
+ style={[trackStyle, style]}
68
+ {...props}
69
+ >
70
+ <Animated.View
71
+ className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform"
72
+ style={thumbStyle}
73
+ />
74
+ </AnimatedPressable>
75
+ );
76
+ }
77
+ );
78
+ Switch.displayName = 'Switch';
@@ -0,0 +1 @@
1
+ export * from './table';