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.
- package/README.md +34 -0
- package/babel.config.js +6 -0
- package/jest.config.js +22 -0
- package/jest.init.js +5 -0
- package/jest.setup.js +173 -0
- package/package.json +87 -0
- package/src/__tests__/a11y/accessibility.test.tsx +33 -0
- package/src/__tests__/components/badge.test.tsx +25 -0
- package/src/__tests__/components/button.test.tsx +53 -0
- package/src/__tests__/components/card.test.tsx +28 -0
- package/src/__tests__/components/input.test.tsx +33 -0
- package/src/__tests__/hooks/use-controllable.test.ts +58 -0
- package/src/__tests__/integration.test.tsx +35 -0
- package/src/__tests__/lib/utils.test.ts +23 -0
- package/src/__tests__/mocks/handlers.ts +19 -0
- package/src/components/accordion/accordion.tsx +143 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.tsx +65 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/alert-dialog/alert-dialog.tsx +145 -0
- package/src/components/alert-dialog/index.ts +1 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +18 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/avatar/avatar.tsx +93 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.tsx +64 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.tsx +75 -0
- package/src/components/breadcrumb/index.ts +1 -0
- package/src/components/button/button.tsx +119 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +40 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/checkbox.tsx +87 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/collapsible/collapsible.tsx +92 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/context-menu/context-menu.tsx +121 -0
- package/src/components/context-menu/index.ts +1 -0
- package/src/components/dialog/dialog.tsx +124 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +145 -0
- package/src/components/dropdown-menu/index.ts +1 -0
- package/src/components/form/form.tsx +84 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +115 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.tsx +13 -0
- package/src/components/navigation-menu/index.ts +1 -0
- package/src/components/navigation-menu/navigation-menu.tsx +68 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.tsx +70 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.tsx +66 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.tsx +90 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +27 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +154 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +37 -0
- package/src/components/sheet/index.ts +1 -0
- package/src/components/sheet/sheet.tsx +128 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +84 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/slider/slider.tsx +145 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +78 -0
- package/src/components/table/index.ts +1 -0
- package/src/components/table/table.tsx +71 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +124 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +83 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +124 -0
- package/src/components/toggle/index.ts +1 -0
- package/src/components/toggle/toggle.tsx +87 -0
- package/src/components/toggle-group/index.ts +1 -0
- package/src/components/toggle-group/toggle-group.tsx +87 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +103 -0
- package/src/components/typography/index.ts +1 -0
- package/src/components/typography/typography.tsx +57 -0
- package/src/context/index.ts +3 -0
- package/src/context/provider.tsx +35 -0
- package/src/context/theme-context.tsx +81 -0
- package/src/context/toast-context.tsx +63 -0
- package/src/env.d.ts +2 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/use-biometric.ts +27 -0
- package/src/hooks/use-color-scheme.ts +10 -0
- package/src/hooks/use-controllable.ts +40 -0
- package/src/hooks/use-countdown.ts +33 -0
- package/src/hooks/use-debounce.ts +18 -0
- package/src/hooks/use-disclosure.ts +14 -0
- package/src/hooks/use-haptics.ts +47 -0
- package/src/hooks/use-keyboard.ts +35 -0
- package/src/hooks/use-media-query.ts +27 -0
- package/src/hooks/use-press-animation.ts +45 -0
- package/src/hooks/use-previous.ts +14 -0
- package/src/hooks/use-scroll-header.ts +42 -0
- package/src/hooks/use-spring.ts +18 -0
- package/src/hooks/use-theme.ts +6 -0
- package/src/hooks/use-toast.ts +6 -0
- package/src/index.ts +53 -0
- package/src/lib/create-animated.tsx +25 -0
- package/src/lib/create-component.tsx +56 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/platform.ts +25 -0
- package/src/lib/types.ts +28 -0
- package/src/lib/utils.ts +35 -0
- package/src/lib/variants.ts +7 -0
- package/src/premium/ai/chat-bubble.tsx +58 -0
- package/src/premium/ai/typing-indicator.tsx +59 -0
- package/src/premium/charts/bar-chart.tsx +66 -0
- package/src/premium/charts/progress-ring.tsx +63 -0
- package/src/premium/glass/glass-bottom-sheet.tsx +50 -0
- package/src/premium/glass/glass-card.tsx +51 -0
- package/src/premium/glass/glass-header.tsx +61 -0
- package/src/premium/glass/glass-panel.tsx +32 -0
- package/src/premium/glass/glass-sidebar.tsx +56 -0
- package/src/premium/index.ts +44 -0
- package/src/premium/index2.ts +13 -0
- package/src/premium/index3.ts +1 -0
- package/src/premium/inputs/color-picker.tsx +92 -0
- package/src/premium/inputs/currency-input.tsx +50 -0
- package/src/premium/inputs/otp-input.tsx +92 -0
- package/src/premium/inputs/phone-input.tsx +58 -0
- package/src/premium/inputs/rating.tsx +51 -0
- package/src/premium/layout/carousel.tsx +57 -0
- package/src/premium/layout/floating-dock.tsx +63 -0
- package/src/premium/layout/masonry-grid.tsx +41 -0
- package/src/premium/layout/parallax-scroll.tsx +81 -0
- package/src/premium/magic/animated-number.tsx +104 -0
- package/src/premium/magic/bento-grid.tsx +55 -0
- package/src/premium/magic/border-beam.tsx +68 -0
- package/src/premium/magic/confetti.tsx +88 -0
- package/src/premium/magic/magic-card.tsx +65 -0
- package/src/premium/magic/meteors.tsx +95 -0
- package/src/premium/magic/ripple.tsx +70 -0
- package/src/premium/magic/shimmer.tsx +58 -0
- package/src/premium/magic/shiny-button.tsx +70 -0
- package/src/premium/mobile/biometric-button.tsx +82 -0
- package/src/premium/mobile/bottom-tab-bar.tsx +81 -0
- package/src/premium/mobile/fab.tsx +74 -0
- package/src/premium/mobile/haptic-pressable.tsx +53 -0
- package/src/premium/mobile/notification-badge.tsx +61 -0
- package/src/premium/mobile/pull-to-refresh.tsx +84 -0
- package/src/premium/mobile/scroll-header.tsx +57 -0
- package/src/premium/mobile/swipe-row.tsx +128 -0
- package/src/premium/mobile/swipeable-card-stack.tsx +121 -0
- package/src/premium/motion/blur-fade.tsx +51 -0
- package/src/premium/motion/fade-up.tsx +34 -0
- package/src/premium/motion/marquee.tsx +67 -0
- package/src/premium/motion/pulsating-button.tsx +95 -0
- package/src/premium/motion/slide-in.tsx +38 -0
- package/src/premium/motion/stagger-children.tsx +28 -0
- package/src/premium/motion/typing-text.tsx +55 -0
- package/src/premium/motion/word-pull-up.tsx +34 -0
- package/src/premium/onboarding/step-indicator.tsx +65 -0
- package/src/tokens/colors.ts +83 -0
- package/src/tokens/global.css +83 -0
- package/src/tokens/index.ts +10 -0
- package/src/tokens/layout.ts +121 -0
- package/src/tokens/motion.ts +94 -0
- package/src/tokens/themes/dark.ts +7 -0
- package/src/tokens/themes/default.ts +8 -0
- package/src/tokens/themes/ocean.ts +28 -0
- package/src/tokens/themes/rose.ts +29 -0
- package/src/tokens/typography.ts +127 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Dimensions, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withSpring,
|
|
7
|
+
withTiming,
|
|
8
|
+
runOnJS,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
|
|
11
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
12
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
13
|
+
import { cn } from '../../lib/utils';
|
|
14
|
+
|
|
15
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
16
|
+
|
|
17
|
+
export interface SwipeRowProps extends ViewProps {
|
|
18
|
+
leftActions?: React.ReactNode;
|
|
19
|
+
rightActions?: React.ReactNode;
|
|
20
|
+
leftThreshold?: number;
|
|
21
|
+
rightThreshold?: number;
|
|
22
|
+
onSwipeLeft?: () => void;
|
|
23
|
+
onSwipeRight?: () => void;
|
|
24
|
+
actionWidth?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const SwipeRow = React.forwardRef<React.ElementRef<typeof View>, SwipeRowProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
className,
|
|
31
|
+
children,
|
|
32
|
+
leftActions,
|
|
33
|
+
rightActions,
|
|
34
|
+
leftThreshold = SCREEN_WIDTH * 0.3,
|
|
35
|
+
rightThreshold = SCREEN_WIDTH * 0.3,
|
|
36
|
+
onSwipeLeft,
|
|
37
|
+
onSwipeRight,
|
|
38
|
+
actionWidth = 80,
|
|
39
|
+
style,
|
|
40
|
+
...props
|
|
41
|
+
},
|
|
42
|
+
ref
|
|
43
|
+
) => {
|
|
44
|
+
const translateX = useSharedValue(0);
|
|
45
|
+
const triggerHaptic = useHaptics();
|
|
46
|
+
const springConfig = useSpring('snappy');
|
|
47
|
+
|
|
48
|
+
const handleRelease = (currentX: number) => {
|
|
49
|
+
'worklet';
|
|
50
|
+
if (rightActions && currentX < -rightThreshold) {
|
|
51
|
+
if (onSwipeLeft) runOnJS(onSwipeLeft)();
|
|
52
|
+
runOnJS(triggerHaptic)('success');
|
|
53
|
+
translateX.value = withSpring(-actionWidth, springConfig);
|
|
54
|
+
} else if (leftActions && currentX > leftThreshold) {
|
|
55
|
+
if (onSwipeRight) runOnJS(onSwipeRight)();
|
|
56
|
+
runOnJS(triggerHaptic)('success');
|
|
57
|
+
translateX.value = withSpring(actionWidth, springConfig);
|
|
58
|
+
} else {
|
|
59
|
+
translateX.value = withSpring(0, springConfig);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const panGestureEvent = (event: PanGestureHandlerGestureEvent) => {
|
|
64
|
+
'worklet';
|
|
65
|
+
const translationX = event.nativeEvent.translationX;
|
|
66
|
+
|
|
67
|
+
// Limit swipe directions based on available actions
|
|
68
|
+
if (!leftActions && translationX > 0) return;
|
|
69
|
+
if (!rightActions && translationX < 0) return;
|
|
70
|
+
|
|
71
|
+
// Apply resistance when swiping past threshold
|
|
72
|
+
let newX = translationX;
|
|
73
|
+
if (translationX > leftThreshold) {
|
|
74
|
+
newX = leftThreshold + (translationX - leftThreshold) * 0.3;
|
|
75
|
+
} else if (translationX < -rightThreshold) {
|
|
76
|
+
newX = -rightThreshold + (translationX + rightThreshold) * 0.3;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
translateX.value = newX;
|
|
80
|
+
|
|
81
|
+
if (event.nativeEvent.state === 5 || event.nativeEvent.state === 3) {
|
|
82
|
+
handleRelease(translationX);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const rStyle = useAnimatedStyle(() => {
|
|
87
|
+
return {
|
|
88
|
+
transform: [{ translateX: translateX.value }],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const leftActionStyle = useAnimatedStyle(() => {
|
|
93
|
+
return {
|
|
94
|
+
opacity: translateX.value > 0 ? 1 : 0,
|
|
95
|
+
transform: [{ translateX: (translateX.value - actionWidth) / 2 }],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const rightActionStyle = useAnimatedStyle(() => {
|
|
100
|
+
return {
|
|
101
|
+
opacity: translateX.value < 0 ? 1 : 0,
|
|
102
|
+
transform: [{ translateX: (translateX.value + actionWidth) / 2 }],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<View ref={ref} className={cn('relative w-full overflow-hidden', className)} style={style} {...props}>
|
|
108
|
+
{/* Background Actions Layer */}
|
|
109
|
+
<View className="absolute inset-0 flex-row justify-between">
|
|
110
|
+
<Animated.View style={[{ width: actionWidth, height: '100%' }, leftActionStyle]}>
|
|
111
|
+
{leftActions}
|
|
112
|
+
</Animated.View>
|
|
113
|
+
<Animated.View style={[{ width: actionWidth, height: '100%' }, rightActionStyle]}>
|
|
114
|
+
{rightActions}
|
|
115
|
+
</Animated.View>
|
|
116
|
+
</View>
|
|
117
|
+
|
|
118
|
+
{/* Foreground Content Layer */}
|
|
119
|
+
<PanGestureHandler onGestureEvent={panGestureEvent as any}>
|
|
120
|
+
<Animated.View style={[rStyle, { width: '100%' }]} className="bg-background">
|
|
121
|
+
{children}
|
|
122
|
+
</Animated.View>
|
|
123
|
+
</PanGestureHandler>
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
SwipeRow.displayName = 'SwipeRow';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Dimensions, StyleSheet } from 'react-native';
|
|
3
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
+
import Animated, {
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withSpring,
|
|
8
|
+
interpolate,
|
|
9
|
+
Extrapolate,
|
|
10
|
+
runOnJS,
|
|
11
|
+
} from 'react-native-reanimated';
|
|
12
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
13
|
+
|
|
14
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
15
|
+
const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.3;
|
|
16
|
+
|
|
17
|
+
export interface SwipeableCardStackProps {
|
|
18
|
+
data: any[];
|
|
19
|
+
renderCard: (item: any, index: number) => React.ReactNode;
|
|
20
|
+
onSwipedLeft?: (item: any) => void;
|
|
21
|
+
onSwipedRight?: (item: any) => void;
|
|
22
|
+
onSwipedAll?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SwipeableCardStack = ({ data, renderCard, onSwipedLeft, onSwipedRight, onSwipedAll }: SwipeableCardStackProps) => {
|
|
26
|
+
const [currentIndex, setCurrentIndex] = React.useState(0);
|
|
27
|
+
const translateX = useSharedValue(0);
|
|
28
|
+
const translateY = useSharedValue(0);
|
|
29
|
+
const triggerHaptic = useHaptics();
|
|
30
|
+
|
|
31
|
+
const handleSwipeComplete = (direction: 'left' | 'right') => {
|
|
32
|
+
const item = data[currentIndex];
|
|
33
|
+
if (direction === 'left') onSwipedLeft?.(item);
|
|
34
|
+
if (direction === 'right') onSwipedRight?.(item);
|
|
35
|
+
|
|
36
|
+
setCurrentIndex((prev) => {
|
|
37
|
+
const next = prev + 1;
|
|
38
|
+
if (next >= data.length) {
|
|
39
|
+
onSwipedAll?.();
|
|
40
|
+
}
|
|
41
|
+
return next;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
translateX.value = 0;
|
|
45
|
+
translateY.value = 0;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const panGesture = Gesture.Pan()
|
|
49
|
+
.onUpdate((event) => {
|
|
50
|
+
translateX.value = event.translationX;
|
|
51
|
+
translateY.value = event.translationY;
|
|
52
|
+
})
|
|
53
|
+
.onEnd((event) => {
|
|
54
|
+
if (Math.abs(event.translationX) > SWIPE_THRESHOLD) {
|
|
55
|
+
// Swiped off screen
|
|
56
|
+
const isRight = event.translationX > 0;
|
|
57
|
+
runOnJS(triggerHaptic)('success');
|
|
58
|
+
translateX.value = withSpring(isRight ? SCREEN_WIDTH : -SCREEN_WIDTH, { velocity: event.velocityX }, () => {
|
|
59
|
+
runOnJS(handleSwipeComplete)(isRight ? 'right' : 'left');
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
// Return to center
|
|
63
|
+
runOnJS(triggerHaptic)('light');
|
|
64
|
+
translateX.value = withSpring(0);
|
|
65
|
+
translateY.value = withSpring(0);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const animatedCardStyle = useAnimatedStyle(() => {
|
|
70
|
+
const rotate = interpolate(translateX.value, [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2], [-10, 0, 10], Extrapolate.CLAMP);
|
|
71
|
+
return {
|
|
72
|
+
transform: [
|
|
73
|
+
{ translateX: translateX.value },
|
|
74
|
+
{ translateY: translateY.value },
|
|
75
|
+
{ rotateZ: `${rotate}deg` },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (currentIndex >= data.length) {
|
|
81
|
+
return null; // or a finished state
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View style={styles.container}>
|
|
86
|
+
{/* Next Card (Background) */}
|
|
87
|
+
{currentIndex + 1 < data.length && (
|
|
88
|
+
<Animated.View style={[styles.card, { transform: [{ scale: 0.95 }, { translateY: 20 }] }]}>
|
|
89
|
+
{renderCard(data[currentIndex + 1], currentIndex + 1)}
|
|
90
|
+
</Animated.View>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Current Card (Foreground) */}
|
|
94
|
+
<GestureDetector gesture={panGesture}>
|
|
95
|
+
<Animated.View style={[styles.card, animatedCardStyle]}>
|
|
96
|
+
{renderCard(data[currentIndex], currentIndex)}
|
|
97
|
+
</Animated.View>
|
|
98
|
+
</GestureDetector>
|
|
99
|
+
</View>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const styles = StyleSheet.create({
|
|
104
|
+
container: {
|
|
105
|
+
flex: 1,
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
},
|
|
109
|
+
card: {
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
width: '90%',
|
|
112
|
+
height: '70%',
|
|
113
|
+
borderRadius: 20,
|
|
114
|
+
backgroundColor: 'white',
|
|
115
|
+
shadowColor: '#000',
|
|
116
|
+
shadowOffset: { width: 0, height: 10 },
|
|
117
|
+
shadowOpacity: 0.1,
|
|
118
|
+
shadowRadius: 20,
|
|
119
|
+
elevation: 10,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withDelay,
|
|
7
|
+
withSpring,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
11
|
+
|
|
12
|
+
export interface BlurFadeProps extends ViewProps {
|
|
13
|
+
delay?: number;
|
|
14
|
+
duration?: number;
|
|
15
|
+
yOffset?: number;
|
|
16
|
+
blurAmount?: number;
|
|
17
|
+
inView?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const BlurFade = React.forwardRef<React.ElementRef<typeof View>, BlurFadeProps>(
|
|
21
|
+
({ className, children, delay = 0, duration = 400, yOffset = 24, blurAmount = 10, inView = true, style, ...props }, ref) => {
|
|
22
|
+
const opacity = useSharedValue(0);
|
|
23
|
+
const translateY = useSharedValue(yOffset);
|
|
24
|
+
// Note: True blur animation requires complex shader setups in React Native or Expo Blur
|
|
25
|
+
// We simulate the effect with opacity + transform which works incredibly well
|
|
26
|
+
|
|
27
|
+
const springConfig = useSpring('smooth');
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (inView) {
|
|
31
|
+
opacity.value = withDelay(delay, withTiming(1, { duration }));
|
|
32
|
+
translateY.value = withDelay(delay, withSpring(0, springConfig));
|
|
33
|
+
} else {
|
|
34
|
+
opacity.value = withTiming(0, { duration });
|
|
35
|
+
translateY.value = withSpring(yOffset, springConfig);
|
|
36
|
+
}
|
|
37
|
+
}, [inView, delay, duration, yOffset, springConfig]);
|
|
38
|
+
|
|
39
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
40
|
+
opacity: opacity.value,
|
|
41
|
+
transform: [{ translateY: translateY.value }],
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Animated.View ref={ref as any} style={[animatedStyle, style]} className={className} {...props}>
|
|
46
|
+
{children}
|
|
47
|
+
</Animated.View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
BlurFade.displayName = 'BlurFade';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
export interface FadeUpProps extends ViewProps {
|
|
6
|
+
delay?: number;
|
|
7
|
+
duration?: number;
|
|
8
|
+
distance?: number;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FadeUp = React.forwardRef<React.ElementRef<typeof View>, FadeUpProps>(
|
|
13
|
+
({ className, delay = 0, duration = 500, distance = 20, children, style, ...props }, ref) => {
|
|
14
|
+
const opacity = useSharedValue(0);
|
|
15
|
+
const translateY = useSharedValue(distance);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
opacity.value = withDelay(delay, withTiming(1, { duration }));
|
|
19
|
+
translateY.value = withDelay(delay, withSpring(0, { damping: 20, stiffness: 100 }));
|
|
20
|
+
}, [delay, duration, distance]);
|
|
21
|
+
|
|
22
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
23
|
+
opacity: opacity.value,
|
|
24
|
+
transform: [{ translateY: translateY.value }],
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Animated.View ref={ref as any} style={[animatedStyle, style]} className={className} {...props}>
|
|
29
|
+
{children}
|
|
30
|
+
</Animated.View>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
FadeUp.displayName = 'FadeUp';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, type ViewProps, Dimensions } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withRepeat,
|
|
7
|
+
withTiming,
|
|
8
|
+
Easing,
|
|
9
|
+
cancelAnimation,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { cn } from '../../lib/utils';
|
|
12
|
+
|
|
13
|
+
export interface MarqueeProps extends ViewProps {
|
|
14
|
+
duration?: number;
|
|
15
|
+
direction?: 'left' | 'right';
|
|
16
|
+
reverse?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
20
|
+
|
|
21
|
+
export const Marquee = React.forwardRef<React.ElementRef<typeof View>, MarqueeProps>(
|
|
22
|
+
({ className, children, duration = 10000, direction = 'left', reverse = false, ...props }, ref) => {
|
|
23
|
+
const [contentWidth, setContentWidth] = useState(0);
|
|
24
|
+
const translateX = useSharedValue(0);
|
|
25
|
+
|
|
26
|
+
const actualDirection = reverse ? (direction === 'left' ? 'right' : 'left') : direction;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (contentWidth > 0) {
|
|
30
|
+
// Reset translation
|
|
31
|
+
translateX.value = actualDirection === 'left' ? 0 : -contentWidth;
|
|
32
|
+
|
|
33
|
+
translateX.value = withRepeat(
|
|
34
|
+
withTiming(actualDirection === 'left' ? -contentWidth : 0, {
|
|
35
|
+
duration: duration,
|
|
36
|
+
easing: Easing.linear,
|
|
37
|
+
}),
|
|
38
|
+
-1, // Infinite loop
|
|
39
|
+
false // No reverse
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return () => cancelAnimation(translateX);
|
|
43
|
+
}, [contentWidth, actualDirection, duration]);
|
|
44
|
+
|
|
45
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
46
|
+
transform: [{ translateX: translateX.value }],
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View ref={ref} className={cn('flex-row overflow-hidden w-full', className)} {...props}>
|
|
51
|
+
<Animated.View
|
|
52
|
+
style={[animatedStyle, { flexDirection: 'row' }]}
|
|
53
|
+
onLayout={(e) => setContentWidth(e.nativeEvent.layout.width)}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</Animated.View>
|
|
57
|
+
{/* Render a duplicate so it loops seamlessly */}
|
|
58
|
+
{contentWidth > 0 && (
|
|
59
|
+
<Animated.View style={[animatedStyle, { flexDirection: 'row', position: 'absolute', left: contentWidth }]}>
|
|
60
|
+
{children}
|
|
61
|
+
</Animated.View>
|
|
62
|
+
)}
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
Marquee.displayName = 'Marquee';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, Pressable, type PressableProps, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withRepeat,
|
|
7
|
+
withTiming,
|
|
8
|
+
withSpring,
|
|
9
|
+
Easing,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
12
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
13
|
+
import { cn } from '../../lib/utils';
|
|
14
|
+
import { Text } from '../../components/typography';
|
|
15
|
+
|
|
16
|
+
export interface PulsatingButtonProps extends Omit<PressableProps, 'style'> {
|
|
17
|
+
label: string;
|
|
18
|
+
pulseColor?: string;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
24
|
+
|
|
25
|
+
export const PulsatingButton = React.forwardRef<React.ElementRef<typeof Pressable>, PulsatingButtonProps>(
|
|
26
|
+
({ className, label, pulseColor, onPress, style, ...props }, ref) => {
|
|
27
|
+
const { theme } = useThemeContext();
|
|
28
|
+
const triggerHaptic = useHaptics();
|
|
29
|
+
const pulseScale = useSharedValue(1);
|
|
30
|
+
const pulseOpacity = useSharedValue(0.5);
|
|
31
|
+
const pressScale = useSharedValue(1);
|
|
32
|
+
|
|
33
|
+
const actualPulseColor = pulseColor || theme.primary.DEFAULT;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
pulseScale.value = withRepeat(
|
|
37
|
+
withTiming(1.5, { duration: 1500, easing: Easing.out(Easing.ease) }),
|
|
38
|
+
-1,
|
|
39
|
+
false
|
|
40
|
+
);
|
|
41
|
+
pulseOpacity.value = withRepeat(
|
|
42
|
+
withTiming(0, { duration: 1500, easing: Easing.out(Easing.ease) }),
|
|
43
|
+
-1,
|
|
44
|
+
false
|
|
45
|
+
);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const ringStyle = useAnimatedStyle(() => ({
|
|
49
|
+
transform: [{ scale: pulseScale.value }],
|
|
50
|
+
opacity: pulseOpacity.value,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const buttonStyle = useAnimatedStyle(() => ({
|
|
54
|
+
transform: [{ scale: pressScale.value }],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const handlePressIn = () => {
|
|
58
|
+
pressScale.value = withSpring(0.95);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handlePressOut = () => {
|
|
62
|
+
pressScale.value = withSpring(1);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View className={cn('relative items-center justify-center', className)} style={style}>
|
|
67
|
+
{/* Pulsating Ring */}
|
|
68
|
+
<Animated.View
|
|
69
|
+
style={[
|
|
70
|
+
{ backgroundColor: actualPulseColor },
|
|
71
|
+
ringStyle,
|
|
72
|
+
]}
|
|
73
|
+
className="absolute inset-0 rounded-full"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
{/* Actual Button */}
|
|
77
|
+
<AnimatedPressable
|
|
78
|
+
ref={ref as any}
|
|
79
|
+
onPressIn={handlePressIn}
|
|
80
|
+
onPressOut={handlePressOut}
|
|
81
|
+
onPress={(e) => {
|
|
82
|
+
triggerHaptic('selection');
|
|
83
|
+
onPress?.(e);
|
|
84
|
+
}}
|
|
85
|
+
className="rounded-full bg-primary px-6 py-3 shadow-lg flex-row items-center justify-center z-10"
|
|
86
|
+
style={buttonStyle}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
<Text className="text-primary-foreground font-semibold text-base">{label}</Text>
|
|
90
|
+
</AnimatedPressable>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
PulsatingButton.displayName = 'PulsatingButton';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withDelay, withSpring } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
export interface SlideInProps extends ViewProps {
|
|
6
|
+
delay?: number;
|
|
7
|
+
direction?: 'left' | 'right' | 'up' | 'down';
|
|
8
|
+
distance?: number;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const SlideIn = React.forwardRef<React.ElementRef<typeof View>, SlideInProps>(
|
|
13
|
+
({ className, delay = 0, direction = 'left', distance = 50, children, style, ...props }, ref) => {
|
|
14
|
+
const translation = useSharedValue(distance);
|
|
15
|
+
|
|
16
|
+
// Determine the axis and sign
|
|
17
|
+
const isX = direction === 'left' || direction === 'right';
|
|
18
|
+
const initialTranslate = (direction === 'left' || direction === 'up') ? distance : -distance;
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
translation.value = initialTranslate;
|
|
22
|
+
translation.value = withDelay(delay, withSpring(0, { damping: 20, stiffness: 150 }));
|
|
23
|
+
}, [delay, direction, distance]);
|
|
24
|
+
|
|
25
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
26
|
+
transform: [
|
|
27
|
+
isX ? { translateX: translation.value } : { translateY: translation.value }
|
|
28
|
+
],
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Animated.View ref={ref as any} style={[animatedStyle, style]} className={className} {...props}>
|
|
33
|
+
{children}
|
|
34
|
+
</Animated.View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
SlideIn.displayName = 'SlideIn';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { FadeUp } from './fade-up';
|
|
4
|
+
|
|
5
|
+
export interface StaggerChildrenProps extends ViewProps {
|
|
6
|
+
staggerDelay?: number;
|
|
7
|
+
initialDelay?: number;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const StaggerChildren = React.forwardRef<React.ElementRef<typeof View>, StaggerChildrenProps>(
|
|
12
|
+
({ className, staggerDelay = 100, initialDelay = 0, children, ...props }, ref) => {
|
|
13
|
+
|
|
14
|
+
// We expect children to be an array of React Elements
|
|
15
|
+
const childrenArray = React.Children.toArray(children);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<View ref={ref} className={className} {...props}>
|
|
19
|
+
{childrenArray.map((child, index) => (
|
|
20
|
+
<FadeUp key={index} delay={initialDelay + index * staggerDelay}>
|
|
21
|
+
{child}
|
|
22
|
+
</FadeUp>
|
|
23
|
+
))}
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
StaggerChildren.displayName = 'StaggerChildren';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { Text } from '../../components/typography';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
export interface TypingTextProps extends ViewProps {
|
|
7
|
+
text: string;
|
|
8
|
+
typingSpeed?: number;
|
|
9
|
+
cursor?: string;
|
|
10
|
+
cursorBlinkSpeed?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const TypingText = React.forwardRef<React.ElementRef<typeof View>, TypingTextProps>(
|
|
14
|
+
({ className, text, typingSpeed = 50, cursor = '|', cursorBlinkSpeed = 500, style, ...props }, ref) => {
|
|
15
|
+
const [displayedText, setDisplayedText] = useState('');
|
|
16
|
+
const [showCursor, setShowCursor] = useState(true);
|
|
17
|
+
const [isTyping, setIsTyping] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let i = 0;
|
|
21
|
+
setIsTyping(true);
|
|
22
|
+
setDisplayedText('');
|
|
23
|
+
|
|
24
|
+
const typingInterval = setInterval(() => {
|
|
25
|
+
if (i < text.length) {
|
|
26
|
+
setDisplayedText((prev) => prev + text.charAt(i));
|
|
27
|
+
i++;
|
|
28
|
+
} else {
|
|
29
|
+
clearInterval(typingInterval);
|
|
30
|
+
setIsTyping(false);
|
|
31
|
+
}
|
|
32
|
+
}, typingSpeed);
|
|
33
|
+
|
|
34
|
+
return () => clearInterval(typingInterval);
|
|
35
|
+
}, [text, typingSpeed]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const cursorInterval = setInterval(() => {
|
|
39
|
+
setShowCursor((prev) => !prev);
|
|
40
|
+
}, cursorBlinkSpeed);
|
|
41
|
+
|
|
42
|
+
return () => clearInterval(cursorInterval);
|
|
43
|
+
}, [cursorBlinkSpeed]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View ref={ref} className={cn('flex-row items-center', className)} style={style} {...props}>
|
|
47
|
+
<Text className="text-foreground">{displayedText}</Text>
|
|
48
|
+
<Text className={cn('text-foreground opacity-100', !showCursor && 'opacity-0')}>
|
|
49
|
+
{cursor}
|
|
50
|
+
</Text>
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
TypingText.displayName = 'TypingText';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { Text } from '../../components/typography';
|
|
4
|
+
import { FadeUp } from './fade-up';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
|
|
7
|
+
export interface WordPullUpProps extends ViewProps {
|
|
8
|
+
text: string;
|
|
9
|
+
delay?: number;
|
|
10
|
+
wordDelay?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const WordPullUp = React.forwardRef<React.ElementRef<typeof View>, WordPullUpProps>(
|
|
14
|
+
({ className, text, delay = 0, wordDelay = 100, style, ...props }, ref) => {
|
|
15
|
+
const words = text.split(' ');
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<View ref={ref} className={cn('flex-row flex-wrap', className)} style={style} {...props}>
|
|
19
|
+
{words.map((word, i) => (
|
|
20
|
+
<FadeUp
|
|
21
|
+
key={i}
|
|
22
|
+
delay={delay + i * wordDelay}
|
|
23
|
+
duration={500}
|
|
24
|
+
distance={20}
|
|
25
|
+
className="mr-1 mb-1"
|
|
26
|
+
>
|
|
27
|
+
<Text className="text-foreground font-medium text-lg">{word}</Text>
|
|
28
|
+
</FadeUp>
|
|
29
|
+
))}
|
|
30
|
+
</View>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
WordPullUp.displayName = 'WordPullUp';
|