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,70 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, StyleSheet, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ withDelay,
10
+ } from 'react-native-reanimated';
11
+ import { cn } from '../../lib/utils';
12
+
13
+ export interface RippleProps extends ViewProps {
14
+ color?: string;
15
+ numRipples?: number;
16
+ }
17
+
18
+ export const Ripple = React.forwardRef<React.ElementRef<typeof View>, RippleProps>(
19
+ ({ className, color = 'rgba(0, 0, 0, 0.1)', numRipples = 3, style, ...props }, ref) => {
20
+ return (
21
+ <View
22
+ ref={ref}
23
+ className={cn('items-center justify-center overflow-hidden', className)}
24
+ style={style}
25
+ {...props}
26
+ >
27
+ {Array.from({ length: numRipples }).map((_, i) => (
28
+ <RippleCircle key={i} delay={i * 1000} color={color} />
29
+ ))}
30
+ </View>
31
+ );
32
+ }
33
+ );
34
+ Ripple.displayName = 'Ripple';
35
+
36
+ const RippleCircle = ({ delay, color }: { delay: number; color: string }) => {
37
+ const scale = useSharedValue(0);
38
+ const opacity = useSharedValue(1);
39
+
40
+ useEffect(() => {
41
+ scale.value = withDelay(
42
+ delay,
43
+ withRepeat(withTiming(4, { duration: 3000, easing: Easing.out(Easing.ease) }), -1, false)
44
+ );
45
+ opacity.value = withDelay(
46
+ delay,
47
+ withRepeat(withTiming(0, { duration: 3000, easing: Easing.out(Easing.ease) }), -1, false)
48
+ );
49
+ }, []);
50
+
51
+ const animatedStyle = useAnimatedStyle(() => ({
52
+ transform: [{ scale: scale.value }],
53
+ opacity: opacity.value,
54
+ }));
55
+
56
+ return (
57
+ <Animated.View
58
+ style={[
59
+ {
60
+ position: 'absolute',
61
+ width: 100,
62
+ height: 100,
63
+ borderRadius: 50,
64
+ backgroundColor: color,
65
+ },
66
+ animatedStyle,
67
+ ]}
68
+ />
69
+ );
70
+ };
@@ -0,0 +1,58 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, StyleSheet, type ViewProps, type DimensionValue } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ interpolate,
10
+ } from 'react-native-reanimated';
11
+ import { LinearGradient } from 'expo-linear-gradient';
12
+
13
+ export interface ShimmerProps extends ViewProps {
14
+ duration?: number;
15
+ width?: DimensionValue;
16
+ height?: DimensionValue;
17
+ borderRadius?: number;
18
+ }
19
+
20
+ export const Shimmer = React.forwardRef<React.ElementRef<typeof View>, ShimmerProps>(
21
+ ({ className, duration = 1500, width = '100%', height = 20, borderRadius = 8, style, ...props }, ref) => {
22
+ const animatedValue = useSharedValue(0);
23
+
24
+ useEffect(() => {
25
+ animatedValue.value = withRepeat(
26
+ withTiming(1, { duration, easing: Easing.linear }),
27
+ -1,
28
+ false
29
+ );
30
+ }, [duration]);
31
+
32
+ const animatedStyle = useAnimatedStyle(() => {
33
+ const translateX = interpolate(animatedValue.value, [0, 1], [-500, 500]); // Rough estimate, ideally measure width
34
+ return {
35
+ transform: [{ translateX }],
36
+ };
37
+ });
38
+
39
+ return (
40
+ <View
41
+ ref={ref}
42
+ style={[{ width, height, borderRadius, backgroundColor: '#E2E8F0', overflow: 'hidden' }, style]}
43
+ className={className}
44
+ {...props}
45
+ >
46
+ <Animated.View style={[StyleSheet.absoluteFill, animatedStyle, { width: 1000 }]}>
47
+ <LinearGradient
48
+ colors={['transparent', 'rgba(255,255,255,0.6)', 'transparent']}
49
+ start={{ x: 0, y: 0.5 }}
50
+ end={{ x: 1, y: 0.5 }}
51
+ style={StyleSheet.absoluteFill}
52
+ />
53
+ </Animated.View>
54
+ </View>
55
+ );
56
+ }
57
+ );
58
+ Shimmer.displayName = 'Shimmer';
@@ -0,0 +1,70 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, Pressable, StyleSheet, type PressableProps, type GestureResponderEvent } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withRepeat,
7
+ withTiming,
8
+ Easing,
9
+ interpolate,
10
+ } from 'react-native-reanimated';
11
+ import { LinearGradient } from 'expo-linear-gradient';
12
+ import { useHaptics } from '../../hooks/use-haptics';
13
+ import { cn } from '../../lib/utils';
14
+ import { Text } from '../../components/typography';
15
+
16
+ export interface ShinyButtonProps extends PressableProps {
17
+ label: string;
18
+ className?: string;
19
+ }
20
+
21
+ const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
22
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
23
+
24
+ export const ShinyButton = React.forwardRef<React.ElementRef<typeof Pressable>, ShinyButtonProps>(
25
+ ({ className, label, style, onPress, ...props }, ref) => {
26
+ const triggerHaptic = useHaptics();
27
+ const animatedValue = useSharedValue(0);
28
+
29
+ useEffect(() => {
30
+ animatedValue.value = withRepeat(
31
+ withTiming(1, { duration: 3000, easing: Easing.linear }),
32
+ -1,
33
+ false
34
+ );
35
+ }, []);
36
+
37
+ const animatedStyle = useAnimatedStyle(() => {
38
+ const translateX = interpolate(animatedValue.value, [0, 1], [-200, 200]);
39
+ return {
40
+ transform: [{ translateX }],
41
+ };
42
+ });
43
+
44
+ return (
45
+ <AnimatedPressable
46
+ ref={ref as any}
47
+ onPress={(e: GestureResponderEvent) => {
48
+ triggerHaptic('light');
49
+ onPress?.(e);
50
+ }}
51
+ className={cn('relative overflow-hidden rounded-full bg-primary px-6 py-3 shadow-md', className)}
52
+ style={style}
53
+ {...props}
54
+ >
55
+ <Text className="text-center font-semibold text-primary-foreground">{label}</Text>
56
+
57
+ {/* Shine Effect */}
58
+ <Animated.View style={[StyleSheet.absoluteFill, animatedStyle, { width: 100, opacity: 0.3 }]}>
59
+ <LinearGradient
60
+ colors={['transparent', 'rgba(255,255,255,1)', 'transparent']}
61
+ start={{ x: 0, y: 0 }}
62
+ end={{ x: 1, y: 0 }}
63
+ style={StyleSheet.absoluteFill}
64
+ />
65
+ </Animated.View>
66
+ </AnimatedPressable>
67
+ );
68
+ }
69
+ );
70
+ ShinyButton.displayName = 'ShinyButton';
@@ -0,0 +1,82 @@
1
+ import React, { useState } from 'react';
2
+ import { View, type ViewProps, ActivityIndicator } from 'react-native';
3
+ import * as LocalAuthentication from 'expo-local-authentication';
4
+ import { HapticPressable } from './haptic-pressable';
5
+ import { cn } from '../../lib/utils';
6
+ import { Text } from '../../components/typography';
7
+ import { Fingerprint, ScanFace } from 'lucide-react-native'; // Assuming these exist or substitute with standard icons
8
+
9
+ export interface BiometricButtonProps extends ViewProps {
10
+ onSuccess: () => void;
11
+ onError?: (error: Error) => void;
12
+ promptMessage?: string;
13
+ fallbackLabel?: string;
14
+ }
15
+
16
+ export const BiometricButton = React.forwardRef<React.ElementRef<typeof View>, BiometricButtonProps>(
17
+ ({ className, onSuccess, onError, promptMessage = 'Authenticate to continue', fallbackLabel = 'Use Passcode', ...props }, ref) => {
18
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
19
+ const [biometricType, setBiometricType] = useState<LocalAuthentication.AuthenticationType | null>(null);
20
+
21
+ React.useEffect(() => {
22
+ (async () => {
23
+ const hasHardware = await LocalAuthentication.hasHardwareAsync();
24
+ const isEnrolled = await LocalAuthentication.isEnrolledAsync();
25
+ if (hasHardware && isEnrolled) {
26
+ const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
27
+ if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
28
+ setBiometricType(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION);
29
+ } else if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
30
+ setBiometricType(LocalAuthentication.AuthenticationType.FINGERPRINT);
31
+ }
32
+ }
33
+ })();
34
+ }, []);
35
+
36
+ const handleAuthenticate = async () => {
37
+ try {
38
+ setIsAuthenticating(true);
39
+ const result = await LocalAuthentication.authenticateAsync({
40
+ promptMessage,
41
+ fallbackLabel,
42
+ disableDeviceFallback: false,
43
+ });
44
+
45
+ if (result.success) {
46
+ onSuccess();
47
+ } else {
48
+ onError?.(new Error(result.error || 'Authentication failed'));
49
+ }
50
+ } catch (err: any) {
51
+ onError?.(err);
52
+ } finally {
53
+ setIsAuthenticating(false);
54
+ }
55
+ };
56
+
57
+ if (!biometricType) return null; // Don't render if biometrics aren't available
58
+
59
+ return (
60
+ <View ref={ref} className={cn('items-center justify-center', className)} {...props}>
61
+ <HapticPressable
62
+ onPress={handleAuthenticate}
63
+ disabled={isAuthenticating}
64
+ hapticStyle="medium"
65
+ className="h-16 w-16 items-center justify-center rounded-full bg-primary/10 transition-colors active:bg-primary/20"
66
+ >
67
+ {isAuthenticating ? (
68
+ <ActivityIndicator color="hsl(var(--primary))" />
69
+ ) : biometricType === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION ? (
70
+ <ScanFace size={32} className="text-primary" />
71
+ ) : (
72
+ <Fingerprint size={32} className="text-primary" />
73
+ )}
74
+ </HapticPressable>
75
+ <Text className="mt-2 text-xs font-medium text-muted-foreground">
76
+ {biometricType === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION ? 'Face ID' : 'Touch ID'}
77
+ </Text>
78
+ </View>
79
+ );
80
+ }
81
+ );
82
+ BiometricButton.displayName = 'BiometricButton';
@@ -0,0 +1,81 @@
1
+ import React, { useState } from 'react';
2
+ import { View, Pressable, type ViewProps, Dimensions } from 'react-native';
3
+ import Animated, { useAnimatedStyle, withSpring, interpolateColor, useSharedValue } from 'react-native-reanimated';
4
+ import { useHaptics } from '../../hooks/use-haptics';
5
+ import { useSpring } from '../../hooks/use-spring';
6
+ import { cn } from '../../lib/utils';
7
+ import { Text } from '../../components/typography';
8
+ import { useThemeContext } from '../../context/theme-context';
9
+
10
+ export interface BottomTabBarItem {
11
+ key: string;
12
+ label: string;
13
+ icon: React.ReactNode;
14
+ }
15
+
16
+ export interface BottomTabBarProps extends ViewProps {
17
+ items: BottomTabBarItem[];
18
+ activeKey: string;
19
+ onTabPress: (key: string) => void;
20
+ }
21
+
22
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
23
+
24
+ export const BottomTabBar = React.forwardRef<React.ElementRef<typeof View>, BottomTabBarProps>(
25
+ ({ className, items, activeKey, onTabPress, ...props }, ref) => {
26
+ const triggerHaptic = useHaptics();
27
+ const springConfig = useSpring('snappy');
28
+ const { theme } = useThemeContext();
29
+ const [containerWidth, setContainerWidth] = useState(SCREEN_WIDTH);
30
+
31
+ const activeIndex = Math.max(0, items.findIndex(i => i.key === activeKey));
32
+ const tabWidth = containerWidth / items.length;
33
+
34
+ const indicatorStyle = useAnimatedStyle(() => {
35
+ return {
36
+ width: withSpring(tabWidth - 24, springConfig),
37
+ transform: [{ translateX: withSpring(activeIndex * tabWidth + 12, springConfig) }],
38
+ };
39
+ }, [activeIndex, tabWidth, springConfig]);
40
+
41
+ return (
42
+ <View
43
+ ref={ref}
44
+ className={cn(
45
+ 'flex-row items-center w-full bg-background border-t border-border pb-safe pt-2',
46
+ className
47
+ )}
48
+ onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
49
+ {...props}
50
+ >
51
+ <Animated.View
52
+ className="absolute top-2 h-10 rounded-full bg-primary/10"
53
+ style={indicatorStyle}
54
+ />
55
+
56
+ {items.map((item, index) => {
57
+ const isActive = index === activeIndex;
58
+
59
+ return (
60
+ <Pressable
61
+ key={item.key}
62
+ onPress={() => {
63
+ triggerHaptic('selection');
64
+ onTabPress(item.key);
65
+ }}
66
+ className="flex-1 items-center justify-center h-12"
67
+ >
68
+ <View className={cn("mb-1", isActive ? "text-primary" : "text-muted-foreground")}>
69
+ {item.icon}
70
+ </View>
71
+ <Text className={cn("text-[10px] font-medium", isActive ? "text-primary" : "text-muted-foreground")}>
72
+ {item.label}
73
+ </Text>
74
+ </Pressable>
75
+ );
76
+ })}
77
+ </View>
78
+ );
79
+ }
80
+ );
81
+ BottomTabBar.displayName = 'BottomTabBar';
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { Pressable, type PressableProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, withSpring, useSharedValue } from 'react-native-reanimated';
4
+ import { useHaptics } from '../../hooks/use-haptics';
5
+ import { useSpring } from '../../hooks/use-spring';
6
+ import { cn } from '../../lib/utils';
7
+ import { Text } from '../../components/typography';
8
+
9
+ export interface FabProps extends PressableProps {
10
+ icon: React.ReactNode;
11
+ label?: string;
12
+ isExpanded?: boolean;
13
+ position?: 'bottom-right' | 'bottom-left' | 'bottom-center';
14
+ }
15
+
16
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
17
+
18
+ export const Fab = React.forwardRef<React.ElementRef<typeof Pressable>, FabProps>(
19
+ ({ className, icon, label, isExpanded = false, position = 'bottom-right', onPress, ...props }, ref) => {
20
+ const triggerHaptic = useHaptics();
21
+ const springConfig = useSpring('bouncy');
22
+ const pressed = useSharedValue(0);
23
+
24
+ const handlePressIn = () => {
25
+ pressed.value = 1;
26
+ };
27
+
28
+ const handlePressOut = () => {
29
+ pressed.value = 0;
30
+ };
31
+
32
+ const animatedStyle = useAnimatedStyle(() => {
33
+ return {
34
+ transform: [{ scale: withSpring(pressed.value ? 0.9 : 1, springConfig) }],
35
+ };
36
+ });
37
+
38
+ const getPositionClasses = () => {
39
+ switch (position) {
40
+ case 'bottom-left': return 'absolute bottom-6 left-6';
41
+ case 'bottom-center': return 'absolute bottom-6 self-center';
42
+ case 'bottom-right':
43
+ default: return 'absolute bottom-6 right-6';
44
+ }
45
+ };
46
+
47
+ return (
48
+ <AnimatedPressable
49
+ ref={ref}
50
+ onPressIn={handlePressIn}
51
+ onPressOut={handlePressOut}
52
+ onPress={(e) => {
53
+ triggerHaptic('light');
54
+ onPress?.(e);
55
+ }}
56
+ className={cn(
57
+ 'flex-row items-center justify-center rounded-full bg-primary p-4 shadow-lg active:bg-primary/90',
58
+ getPositionClasses(),
59
+ className
60
+ )}
61
+ style={animatedStyle}
62
+ {...props}
63
+ >
64
+ {icon}
65
+ {label && isExpanded && (
66
+ <Text className="ml-2 font-semibold text-primary-foreground">
67
+ {label}
68
+ </Text>
69
+ )}
70
+ </AnimatedPressable>
71
+ );
72
+ }
73
+ );
74
+ Fab.displayName = 'Fab';
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Pressable, type PressableProps } from 'react-native';
3
+ import { useHaptics } from '../../hooks/use-haptics';
4
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
5
+
6
+ export interface HapticPressableProps extends PressableProps {
7
+ hapticStyle?: 'light' | 'medium' | 'heavy' | 'selection' | 'success' | 'warning' | 'error';
8
+ scaleOnPress?: boolean;
9
+ }
10
+
11
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
12
+
13
+ export const HapticPressable = React.forwardRef<React.ElementRef<typeof Pressable>, HapticPressableProps>(
14
+ ({ onPress, onPressIn, onPressOut, hapticStyle = 'light', scaleOnPress = true, style, ...props }, ref) => {
15
+ const triggerHaptic = useHaptics();
16
+ const scale = useSharedValue(1);
17
+
18
+ const handlePress = (e: any) => {
19
+ triggerHaptic(hapticStyle);
20
+ onPress?.(e);
21
+ };
22
+
23
+ const handlePressIn = (e: any) => {
24
+ if (scaleOnPress) {
25
+ scale.value = withSpring(0.96, { damping: 15, stiffness: 300 });
26
+ }
27
+ onPressIn?.(e);
28
+ };
29
+
30
+ const handlePressOut = (e: any) => {
31
+ if (scaleOnPress) {
32
+ scale.value = withSpring(1, { damping: 15, stiffness: 300 });
33
+ }
34
+ onPressOut?.(e);
35
+ };
36
+
37
+ const animatedStyle = useAnimatedStyle(() => ({
38
+ transform: [{ scale: scale.value }],
39
+ }));
40
+
41
+ return (
42
+ <AnimatedPressable
43
+ ref={ref as any}
44
+ onPress={handlePress}
45
+ onPressIn={handlePressIn}
46
+ onPressOut={handlePressOut}
47
+ style={[scaleOnPress ? animatedStyle : undefined, style]}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ );
53
+ HapticPressable.displayName = 'HapticPressable';
@@ -0,0 +1,61 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ withSequence,
8
+ withDelay,
9
+ } from 'react-native-reanimated';
10
+ import { cn } from '../../lib/utils';
11
+ import { Text } from '../../components/typography';
12
+
13
+ export interface NotificationBadgeProps extends ViewProps {
14
+ count?: number;
15
+ maxCount?: number;
16
+ showZero?: boolean;
17
+ }
18
+
19
+ export const NotificationBadge = React.forwardRef<React.ElementRef<typeof View>, NotificationBadgeProps>(
20
+ ({ className, count = 0, maxCount = 99, showZero = false, style, ...props }, ref) => {
21
+ const scale = useSharedValue(0);
22
+ const displayCount = count > maxCount ? `${maxCount}+` : count.toString();
23
+ const shouldShow = count > 0 || showZero;
24
+
25
+ useEffect(() => {
26
+ if (shouldShow) {
27
+ // Pop in animation
28
+ scale.value = withSequence(
29
+ withSpring(1.2, { damping: 12, stiffness: 200 }),
30
+ withSpring(1, { damping: 15, stiffness: 300 })
31
+ );
32
+ } else {
33
+ // Shrink out
34
+ scale.value = withSpring(0);
35
+ }
36
+ }, [count, shouldShow]);
37
+
38
+ const animatedStyle = useAnimatedStyle(() => ({
39
+ transform: [{ scale: scale.value }],
40
+ }));
41
+
42
+ if (!shouldShow && scale.value === 0) return null;
43
+
44
+ return (
45
+ <Animated.View
46
+ ref={ref as any}
47
+ style={[animatedStyle, style]}
48
+ className={cn(
49
+ 'absolute -right-2 -top-2 z-10 flex min-h-[20px] min-w-[20px] items-center justify-center rounded-full bg-destructive px-1 ring-2 ring-background',
50
+ className
51
+ )}
52
+ {...props}
53
+ >
54
+ <Text className="text-[10px] font-bold text-destructive-foreground leading-none">
55
+ {displayCount}
56
+ </Text>
57
+ </Animated.View>
58
+ );
59
+ }
60
+ );
61
+ NotificationBadge.displayName = 'NotificationBadge';
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { View, type ViewProps, type NativeSyntheticEvent, type NativeScrollEvent, StyleSheet } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ interpolate,
8
+ Extrapolate,
9
+ runOnJS,
10
+ withTiming,
11
+ type SharedValue,
12
+ } from 'react-native-reanimated';
13
+ import { RefreshCw } from 'lucide-react-native';
14
+ import { useHaptics } from '../../hooks/use-haptics';
15
+ import { useSpring } from '../../hooks/use-spring';
16
+
17
+ export interface PullToRefreshProps extends ViewProps {
18
+ isRefreshing: boolean;
19
+ onRefresh: () => void;
20
+ /** Pass the Y scroll offset from your ScrollView/FlatList here */
21
+ scrollY: SharedValue<number>;
22
+ threshold?: number;
23
+ }
24
+
25
+ export const PullToRefresh = ({ isRefreshing, onRefresh, scrollY, threshold = 80, ...props }: PullToRefreshProps) => {
26
+ const triggerHaptic = useHaptics();
27
+ const hasTriggeredHaptic = useSharedValue(false);
28
+ const rotation = useSharedValue(0);
29
+
30
+ React.useEffect(() => {
31
+ if (isRefreshing) {
32
+ rotation.value = withTiming(rotation.value + 360, { duration: 1000 }, () => {
33
+ // Simple loop if still refreshing
34
+ if (isRefreshing) rotation.value += 360;
35
+ });
36
+ }
37
+ }, [isRefreshing]);
38
+
39
+ const animatedStyle = useAnimatedStyle(() => {
40
+ const pullDistance = Math.max(0, -scrollY.value);
41
+
42
+ // Trigger haptic once when crossing threshold
43
+ if (pullDistance >= threshold && !hasTriggeredHaptic.value && !isRefreshing) {
44
+ hasTriggeredHaptic.value = true;
45
+ runOnJS(triggerHaptic)('success');
46
+ } else if (pullDistance < threshold) {
47
+ hasTriggeredHaptic.value = false;
48
+ }
49
+
50
+ const scale = interpolate(pullDistance, [0, threshold], [0.5, 1], Extrapolate.CLAMP);
51
+ const opacity = interpolate(pullDistance, [0, threshold / 2, threshold], [0, 0.5, 1], Extrapolate.CLAMP);
52
+
53
+ return {
54
+ opacity: isRefreshing ? 1 : opacity,
55
+ transform: [
56
+ { scale: isRefreshing ? 1 : scale },
57
+ { translateY: isRefreshing ? threshold / 2 : Math.min(pullDistance, threshold) / 2 },
58
+ ],
59
+ };
60
+ });
61
+
62
+ const iconStyle = useAnimatedStyle(() => ({
63
+ transform: [{ rotateZ: `${rotation.value}deg` }],
64
+ }));
65
+
66
+ return (
67
+ <Animated.View
68
+ style={[
69
+ StyleSheet.absoluteFill,
70
+ { height: threshold, alignItems: 'center', justifyContent: 'center' },
71
+ animatedStyle,
72
+ ]}
73
+ pointerEvents="none"
74
+ {...props}
75
+ >
76
+ <View className="h-10 w-10 items-center justify-center rounded-full bg-background shadow-md border border-border">
77
+ <Animated.View style={iconStyle}>
78
+ <RefreshCw size={20} className="text-primary" />
79
+ </Animated.View>
80
+ </View>
81
+ </Animated.View>
82
+ );
83
+ };
84
+ PullToRefresh.displayName = 'PullToRefresh';
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ interpolate,
8
+ Extrapolate,
9
+ type SharedValue
10
+ } from 'react-native-reanimated';
11
+ import { cn } from '../../lib/utils';
12
+
13
+ export interface ScrollHeaderProps extends ViewProps {
14
+ scrollY: SharedValue<number>;
15
+ headerHeight?: number;
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ export const ScrollHeader = React.forwardRef<React.ElementRef<typeof View>, ScrollHeaderProps>(
20
+ ({ className, scrollY, headerHeight = 100, children, style, ...props }, ref) => {
21
+
22
+ const animatedStyle = useAnimatedStyle(() => {
23
+ const translateY = interpolate(
24
+ scrollY.value,
25
+ [0, headerHeight],
26
+ [0, -headerHeight],
27
+ Extrapolate.CLAMP
28
+ );
29
+
30
+ const opacity = interpolate(
31
+ scrollY.value,
32
+ [0, headerHeight * 0.8],
33
+ [1, 0],
34
+ Extrapolate.CLAMP
35
+ );
36
+
37
+ return {
38
+ transform: [{ translateY }],
39
+ opacity,
40
+ };
41
+ });
42
+
43
+ return (
44
+ <Animated.View
45
+ ref={ref as any}
46
+ className={cn('absolute top-0 left-0 right-0 z-50 bg-background border-b border-border pt-safe', className)}
47
+ style={[{ height: headerHeight }, animatedStyle, style]}
48
+ {...props}
49
+ >
50
+ <View className="flex-1 justify-end pb-4 px-4">
51
+ {children}
52
+ </View>
53
+ </Animated.View>
54
+ );
55
+ }
56
+ );
57
+ ScrollHeader.displayName = 'ScrollHeader';