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,119 @@
1
+ import React from 'react';
2
+ import { Pressable, View, type PressableProps } from 'react-native';
3
+ import { cva, type VariantProps } from '../../lib/variants';
4
+ import { createComponent } from '../../lib/create-component';
5
+ import { Text } from '../typography';
6
+ import { usePressAnimation } from '../../hooks/use-press-animation';
7
+ import { useHaptics } from '../../hooks/use-haptics';
8
+ import Animated from 'react-native-reanimated';
9
+
10
+ const buttonVariants = cva(
11
+ 'flex-row items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
16
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
17
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
18
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ // Nativecn extensions
22
+ gradient: 'bg-transparent', // Handled by a Gradient wrapper in Layer 3
23
+ glass: 'bg-glass border border-glass-border',
24
+ },
25
+ size: {
26
+ default: 'h-10 px-4 py-2',
27
+ sm: 'h-9 rounded-md px-3',
28
+ lg: 'h-11 rounded-md px-8',
29
+ icon: 'h-10 w-10',
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: 'default',
34
+ size: 'default',
35
+ },
36
+ }
37
+ );
38
+
39
+ // Map variants to specific text styles to override inheritance issues in React Native
40
+ const textVariants = cva('', {
41
+ variants: {
42
+ variant: {
43
+ default: 'text-primary-foreground',
44
+ destructive: 'text-destructive-foreground',
45
+ outline: 'text-foreground',
46
+ secondary: 'text-secondary-foreground',
47
+ ghost: 'text-foreground',
48
+ link: 'text-primary underline',
49
+ gradient: 'text-primary-foreground',
50
+ glass: 'text-foreground',
51
+ },
52
+ size: {
53
+ default: 'text-sm font-medium',
54
+ sm: 'text-xs font-medium',
55
+ lg: 'text-base font-medium',
56
+ icon: 'text-sm',
57
+ },
58
+ },
59
+ defaultVariants: {
60
+ variant: 'default',
61
+ size: 'default',
62
+ },
63
+ });
64
+
65
+ export interface ButtonProps
66
+ extends React.ComponentPropsWithoutRef<typeof Pressable>,
67
+ VariantProps<typeof buttonVariants> {
68
+ haptic?: 'none' | 'selection' | 'light' | 'medium' | 'heavy';
69
+ animated?: boolean;
70
+ }
71
+
72
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
73
+
74
+ export const Button = React.forwardRef<React.ElementRef<typeof Pressable>, ButtonProps>(
75
+ ({ className, variant, size, haptic = 'selection', animated = true, children, onPressIn, onPressOut, onPress, ...props }, ref) => {
76
+ const triggerHaptic = useHaptics();
77
+ const { handlePressIn, handlePressOut, animatedStyle } = usePressAnimation({
78
+ scale: 'normal',
79
+ springConfig: 'snappy',
80
+ });
81
+
82
+ const onInternalPressIn = (e: any) => {
83
+ if (animated) handlePressIn();
84
+ onPressIn?.(e);
85
+ };
86
+
87
+ const onInternalPressOut = (e: any) => {
88
+ if (animated) handlePressOut();
89
+ onPressOut?.(e);
90
+ };
91
+
92
+ const onInternalPress = (e: any) => {
93
+ if (haptic !== 'none') triggerHaptic(haptic);
94
+ onPress?.(e);
95
+ };
96
+
97
+ const Container = animated ? AnimatedPressable : Pressable;
98
+ const style = animated ? animatedStyle : undefined;
99
+
100
+ return (
101
+ <Container
102
+ ref={ref}
103
+ className={buttonVariants({ variant, size, className })}
104
+ onPressIn={onInternalPressIn}
105
+ onPressOut={onInternalPressOut}
106
+ onPress={onInternalPress}
107
+ style={style}
108
+ {...props}
109
+ >
110
+ {typeof children === 'string' ? (
111
+ <Text className={textVariants({ variant, size })}>{children}</Text>
112
+ ) : (
113
+ children
114
+ )}
115
+ </Container>
116
+ );
117
+ }
118
+ );
119
+ Button.displayName = 'Button';
@@ -0,0 +1 @@
1
+ export * from './button';
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { createComponent } from '../../lib/create-component';
4
+ import { Text } from '../typography';
5
+
6
+ const Card = createComponent<View, ViewProps>({
7
+ Component: View,
8
+ baseClassName: 'rounded-lg border border-border bg-card shadow-sm',
9
+ });
10
+
11
+ const CardHeader = createComponent<View, ViewProps>({
12
+ Component: View,
13
+ baseClassName: 'flex flex-col space-y-1.5 p-6',
14
+ });
15
+
16
+ const CardTitle = React.forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
17
+ ({ className, ...props }, ref) => (
18
+ <Text ref={ref} className={`text-2xl font-semibold leading-none tracking-tight ${className}`} {...props} />
19
+ )
20
+ );
21
+ CardTitle.displayName = 'CardTitle';
22
+
23
+ const CardDescription = React.forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
24
+ ({ className, ...props }, ref) => (
25
+ <Text ref={ref} className={`text-sm text-muted-foreground ${className}`} {...props} />
26
+ )
27
+ );
28
+ CardDescription.displayName = 'CardDescription';
29
+
30
+ const CardContent = createComponent<View, ViewProps>({
31
+ Component: View,
32
+ baseClassName: 'p-6 pt-0',
33
+ });
34
+
35
+ const CardFooter = createComponent<View, ViewProps>({
36
+ Component: View,
37
+ baseClassName: 'flex flex-row items-center p-6 pt-0',
38
+ });
39
+
40
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1 @@
1
+ export * from './card';
@@ -0,0 +1,87 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { Pressable, type PressableProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, withSpring, interpolate } from 'react-native-reanimated';
4
+ import { Check as CheckIcon } from 'lucide-react-native';
5
+ import { useControllableState } from '../../hooks/use-controllable';
6
+ import { useHaptics } from '../../hooks/use-haptics';
7
+ import { useSpring } from '../../hooks/use-spring';
8
+ import { useThemeContext } from '../../context/theme-context';
9
+ import { cn } from '../../lib/utils';
10
+
11
+ export interface CheckboxProps extends Omit<PressableProps, 'value'> {
12
+ checked?: boolean | 'indeterminate';
13
+ defaultChecked?: boolean | 'indeterminate';
14
+ onCheckedChange?: (checked: boolean | 'indeterminate') => void;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
19
+
20
+ export const Checkbox = forwardRef<React.ElementRef<typeof Pressable>, CheckboxProps>(
21
+ ({ className, checked: checkedProp, defaultChecked, onCheckedChange, disabled, style, ...props }, ref) => {
22
+ const triggerHaptic = useHaptics();
23
+ const springConfig = useSpring('snappy');
24
+ const { theme } = useThemeContext();
25
+
26
+ const [checked, setChecked] = useControllableState({
27
+ prop: checkedProp,
28
+ defaultProp: defaultChecked || false,
29
+ onChange: onCheckedChange,
30
+ });
31
+
32
+ const isChecked = checked === true;
33
+ const isIndeterminate = checked === 'indeterminate';
34
+
35
+ const handlePress = () => {
36
+ if (disabled) return;
37
+ triggerHaptic('selection');
38
+ if (isIndeterminate) {
39
+ setChecked(true);
40
+ } else {
41
+ setChecked(!checked);
42
+ }
43
+ };
44
+
45
+ const containerStyle = useAnimatedStyle(() => {
46
+ return {
47
+ backgroundColor: (isChecked || isIndeterminate) ? theme.primary.DEFAULT : 'transparent',
48
+ borderColor: (isChecked || isIndeterminate) ? theme.primary.DEFAULT : theme.primary.DEFAULT,
49
+ };
50
+ }, [isChecked, isIndeterminate, theme]);
51
+
52
+ const iconStyle = useAnimatedStyle(() => {
53
+ const scale = withSpring(isChecked || isIndeterminate ? 1 : 0, springConfig);
54
+ const opacity = withSpring(isChecked || isIndeterminate ? 1 : 0, springConfig);
55
+ return {
56
+ transform: [{ scale }],
57
+ opacity,
58
+ };
59
+ }, [isChecked, isIndeterminate, springConfig]);
60
+
61
+ return (
62
+ <AnimatedPressable
63
+ ref={ref}
64
+ onPress={handlePress}
65
+ disabled={disabled}
66
+ accessibilityRole="checkbox"
67
+ accessibilityState={{ checked: isChecked, disabled }}
68
+ className={cn(
69
+ 'peer h-5 w-5 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 justify-center items-center',
70
+ disabled && 'cursor-not-allowed opacity-50',
71
+ className
72
+ )}
73
+ style={[containerStyle, style]}
74
+ {...props}
75
+ >
76
+ <Animated.View style={iconStyle} className="flex items-center justify-center text-current">
77
+ {isIndeterminate ? (
78
+ <Animated.View className="h-0.5 w-2.5 bg-primary-foreground rounded-full" />
79
+ ) : (
80
+ <CheckIcon size={14} color={theme.primary.foreground as string} strokeWidth={3} />
81
+ )}
82
+ </Animated.View>
83
+ </AnimatedPressable>
84
+ );
85
+ }
86
+ );
87
+ Checkbox.displayName = 'Checkbox';
@@ -0,0 +1 @@
1
+ export * from './checkbox';
@@ -0,0 +1,92 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
4
+ import { useSpring } from '../../hooks/use-spring';
5
+ import { cn } from '../../lib/utils';
6
+ import { useControllableState } from '../../hooks/use-controllable';
7
+
8
+ const CollapsibleContext = React.createContext<{
9
+ isOpen: boolean;
10
+ toggle: () => void;
11
+ } | null>(null);
12
+
13
+ export interface CollapsibleProps extends ViewProps {
14
+ open?: boolean;
15
+ defaultOpen?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ }
18
+
19
+ export const Collapsible = forwardRef<React.ElementRef<typeof View>, CollapsibleProps>(
20
+ ({ className, open: openProp, defaultOpen, onOpenChange, children, ...props }, ref) => {
21
+ const [isOpen, setIsOpen] = useControllableState({
22
+ prop: openProp,
23
+ defaultProp: defaultOpen || false,
24
+ onChange: onOpenChange,
25
+ });
26
+
27
+ return (
28
+ <CollapsibleContext.Provider value={{ isOpen, toggle: () => setIsOpen(!isOpen) }}>
29
+ <View ref={ref} className={cn('', className)} {...props}>
30
+ {children}
31
+ </View>
32
+ </CollapsibleContext.Provider>
33
+ );
34
+ }
35
+ );
36
+ Collapsible.displayName = 'Collapsible';
37
+
38
+ export const CollapsibleTrigger = forwardRef<React.ElementRef<typeof Pressable>, PressableProps>(
39
+ ({ className, children, ...props }, ref) => {
40
+ const context = React.useContext(CollapsibleContext);
41
+ if (!context) throw new Error('CollapsibleTrigger must be used within Collapsible');
42
+
43
+ return (
44
+ <Pressable ref={ref} onPress={context.toggle} className={className} {...props}>
45
+ {children}
46
+ </Pressable>
47
+ );
48
+ }
49
+ );
50
+ CollapsibleTrigger.displayName = 'CollapsibleTrigger';
51
+
52
+ export const CollapsibleContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
53
+ ({ className, children, ...props }, ref) => {
54
+ const context = React.useContext(CollapsibleContext);
55
+ if (!context) throw new Error('CollapsibleContent must be used within Collapsible');
56
+
57
+ const [contentHeight, setContentHeight] = useState(0);
58
+ const height = useSharedValue(context.isOpen ? contentHeight : 0);
59
+ const opacity = useSharedValue(context.isOpen ? 1 : 0);
60
+ const springConfig = useSpring('snappy');
61
+
62
+ React.useEffect(() => {
63
+ height.value = withSpring(context.isOpen ? contentHeight : 0, springConfig);
64
+ opacity.value = withSpring(context.isOpen ? 1 : 0, springConfig);
65
+ }, [context.isOpen, contentHeight, springConfig]);
66
+
67
+ const animatedStyle = useAnimatedStyle(() => ({
68
+ height: height.value,
69
+ opacity: opacity.value,
70
+ }));
71
+
72
+ return (
73
+ <Animated.View style={[animatedStyle, { overflow: 'hidden' }]}>
74
+ <View
75
+ ref={ref}
76
+ onLayout={(e) => {
77
+ const h = e.nativeEvent.layout.height;
78
+ if (h > 0 && contentHeight !== h) {
79
+ setContentHeight(h);
80
+ if (context.isOpen) height.value = h;
81
+ }
82
+ }}
83
+ className={className}
84
+ {...props}
85
+ >
86
+ {children}
87
+ </View>
88
+ </Animated.View>
89
+ );
90
+ }
91
+ );
92
+ CollapsibleContent.displayName = 'CollapsibleContent';
@@ -0,0 +1 @@
1
+ export * from './collapsible';
@@ -0,0 +1,121 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import { View, Pressable, Modal, type ViewProps, type LayoutRectangle, type GestureResponderEvent } 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 ContextMenuContext = React.createContext<{
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ triggerPosition: { x: number; y: number } | null;
13
+ setTriggerPosition: (pos: { x: number; y: number } | null) => void;
14
+ } | null>(null);
15
+
16
+ export const ContextMenu = ({ open: openProp, defaultOpen, onOpenChange, children }: any) => {
17
+ const [open, setOpen] = useControllableState({
18
+ prop: openProp,
19
+ defaultProp: defaultOpen || false,
20
+ onChange: onOpenChange,
21
+ });
22
+ const [triggerPosition, setTriggerPosition] = useState<{ x: number; y: number } | null>(null);
23
+
24
+ return (
25
+ <ContextMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerPosition, setTriggerPosition }}>
26
+ {children}
27
+ </ContextMenuContext.Provider>
28
+ );
29
+ };
30
+
31
+ export const ContextMenuTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
32
+ ({ children, ...props }, ref) => {
33
+ const context = React.useContext(ContextMenuContext);
34
+ if (!context) throw new Error('ContextMenuTrigger must be used within ContextMenu');
35
+ const triggerHaptic = useHaptics();
36
+
37
+ const handleLongPress = (e: GestureResponderEvent) => {
38
+ triggerHaptic('heavy');
39
+ const { pageX, pageY } = e.nativeEvent;
40
+ context.setTriggerPosition({ x: pageX, y: pageY });
41
+ context.onOpenChange(true);
42
+ };
43
+
44
+ return (
45
+ <View ref={ref} collapsable={false}>
46
+ <Pressable onLongPress={handleLongPress} delayLongPress={500} {...props}>
47
+ {children}
48
+ </Pressable>
49
+ </View>
50
+ );
51
+ }
52
+ );
53
+ ContextMenuTrigger.displayName = 'ContextMenuTrigger';
54
+
55
+ export const ContextMenuContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
56
+ ({ className, children, ...props }, ref) => {
57
+ const context = React.useContext(ContextMenuContext);
58
+ if (!context) throw new Error('ContextMenuContent must be used within ContextMenu');
59
+
60
+ if (!context.triggerPosition) return null;
61
+
62
+ const { x, y } = context.triggerPosition;
63
+
64
+ return (
65
+ <Modal
66
+ visible={context.open}
67
+ transparent
68
+ animationType="none"
69
+ onRequestClose={() => context.onOpenChange(false)}
70
+ >
71
+ <View className="flex-1">
72
+ <Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
73
+ <Animated.View
74
+ entering={ZoomIn.duration(150)}
75
+ exiting={FadeOut.duration(100)}
76
+ style={{
77
+ position: 'absolute',
78
+ top: y,
79
+ left: x,
80
+ }}
81
+ className={cn(
82
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
83
+ className
84
+ )}
85
+ {...props}
86
+ >
87
+ {children}
88
+ </Animated.View>
89
+ </View>
90
+ </Modal>
91
+ );
92
+ }
93
+ );
94
+ ContextMenuContent.displayName = 'ContextMenuContent';
95
+
96
+ export const ContextMenuItem = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable> & { inset?: boolean }>(
97
+ ({ className, inset, onPress, children, ...props }, ref) => {
98
+ const context = React.useContext(ContextMenuContext);
99
+ const triggerHaptic = useHaptics();
100
+
101
+ return (
102
+ <Pressable
103
+ ref={ref}
104
+ onPress={(e) => {
105
+ triggerHaptic('selection');
106
+ onPress?.(e);
107
+ context?.onOpenChange(false);
108
+ }}
109
+ className={cn(
110
+ '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',
111
+ inset && 'pl-8',
112
+ className
113
+ )}
114
+ {...props}
115
+ >
116
+ <Text className="text-sm font-medium text-popover-foreground">{children as any}</Text>
117
+ </Pressable>
118
+ );
119
+ }
120
+ );
121
+ ContextMenuItem.displayName = 'ContextMenuItem';
@@ -0,0 +1 @@
1
+ export * from './context-menu';
@@ -0,0 +1,124 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { View, Modal, Pressable, type ViewProps, type ModalProps } from 'react-native';
3
+ import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } 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 DialogContext = React.createContext<{
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ } | null>(null);
13
+
14
+ export interface DialogProps {
15
+ open?: boolean;
16
+ defaultOpen?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ children: React.ReactNode;
19
+ }
20
+
21
+ export const Dialog = ({ open: openProp, defaultOpen, onOpenChange, children }: DialogProps) => {
22
+ const [open, setOpen] = useControllableState({
23
+ prop: openProp,
24
+ defaultProp: defaultOpen || false,
25
+ onChange: onOpenChange,
26
+ });
27
+
28
+ return (
29
+ <DialogContext.Provider value={{ open, onOpenChange: setOpen }}>
30
+ {children}
31
+ </DialogContext.Provider>
32
+ );
33
+ };
34
+
35
+ export const DialogTrigger = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable>>(
36
+ ({ children, onPress, ...props }, ref) => {
37
+ const context = React.useContext(DialogContext);
38
+ if (!context) throw new Error('DialogTrigger must be used within Dialog');
39
+
40
+ return (
41
+ <Pressable ref={ref} onPress={(e) => { context.onOpenChange(true); onPress?.(e); }} {...props}>
42
+ {children}
43
+ </Pressable>
44
+ );
45
+ }
46
+ );
47
+ DialogTrigger.displayName = 'DialogTrigger';
48
+
49
+ export interface DialogContentProps extends ViewProps {
50
+ overlayClassName?: string;
51
+ hideClose?: boolean;
52
+ }
53
+
54
+ export const DialogContent = forwardRef<React.ElementRef<typeof View>, DialogContentProps>(
55
+ ({ className, overlayClassName, children, hideClose = false, ...props }, ref) => {
56
+ const context = React.useContext(DialogContext);
57
+ if (!context) throw new Error('DialogContent must be used within Dialog');
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 items-center justify-center', overlayClassName)}>
67
+ <Animated.View
68
+ entering={FadeIn.duration(200)}
69
+ exiting={FadeOut.duration(200)}
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={ZoomIn.duration(200).springify().damping(20).stiffness(200)}
77
+ exiting={ZoomOut.duration(200)}
78
+ ref={ref}
79
+ className={cn(
80
+ 'z-50 grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full sm:w-[90%] w-[90%] rounded-xl mx-4',
81
+ className
82
+ )}
83
+ {...props}
84
+ >
85
+ {children}
86
+ {!hideClose && (
87
+ <Pressable
88
+ onPress={() => context.onOpenChange(false)}
89
+ 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"
90
+ >
91
+ <X size={20} className="text-muted-foreground" />
92
+ </Pressable>
93
+ )}
94
+ </Animated.View>
95
+ </View>
96
+ </Modal>
97
+ );
98
+ }
99
+ );
100
+ DialogContent.displayName = 'DialogContent';
101
+
102
+ export const DialogHeader = ({ className, ...props }: ViewProps) => (
103
+ <View className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
104
+ );
105
+ DialogHeader.displayName = 'DialogHeader';
106
+
107
+ export const DialogFooter = ({ className, ...props }: ViewProps) => (
108
+ <View className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-2 sm:gap-0 mt-4', className)} {...props} />
109
+ );
110
+ DialogFooter.displayName = 'DialogFooter';
111
+
112
+ export const DialogTitle = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
113
+ ({ className, ...props }, ref) => (
114
+ <Text ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
115
+ )
116
+ );
117
+ DialogTitle.displayName = 'DialogTitle';
118
+
119
+ export const DialogDescription = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
120
+ ({ className, ...props }, ref) => (
121
+ <Text ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
122
+ )
123
+ );
124
+ DialogDescription.displayName = 'DialogDescription';
@@ -0,0 +1 @@
1
+ export * from './dialog';