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,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Pressable, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
6
|
+
|
|
7
|
+
export interface FloatingDockItem {
|
|
8
|
+
icon: React.ReactNode;
|
|
9
|
+
onPress: () => void;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FloatingDockProps extends ViewProps {
|
|
14
|
+
items: FloatingDockItem[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
18
|
+
|
|
19
|
+
export const FloatingDock = React.forwardRef<React.ElementRef<typeof View>, FloatingDockProps>(
|
|
20
|
+
({ className, items, ...props }, ref) => {
|
|
21
|
+
return (
|
|
22
|
+
<View
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn('absolute bottom-8 self-center rounded-2xl bg-background/80 p-2 border border-border shadow-lg flex-row items-center gap-2', className)}
|
|
25
|
+
style={{ backdropFilter: 'blur(10px)' } as any}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{items.map((item, index) => (
|
|
29
|
+
<DockItem key={index} item={item} />
|
|
30
|
+
))}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
FloatingDock.displayName = 'FloatingDock';
|
|
36
|
+
|
|
37
|
+
const DockItem = ({ item }: { item: FloatingDockItem }) => {
|
|
38
|
+
const triggerHaptic = useHaptics();
|
|
39
|
+
const scale = useSharedValue(1);
|
|
40
|
+
const translateY = useSharedValue(0);
|
|
41
|
+
|
|
42
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
43
|
+
transform: [{ scale: scale.value }, { translateY: translateY.value }],
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<AnimatedPressable
|
|
48
|
+
onPressIn={() => {
|
|
49
|
+
triggerHaptic('light');
|
|
50
|
+
scale.value = withSpring(1.2);
|
|
51
|
+
translateY.value = withSpring(-10);
|
|
52
|
+
}}
|
|
53
|
+
onPressOut={() => {
|
|
54
|
+
scale.value = withSpring(1);
|
|
55
|
+
translateY.value = withSpring(0);
|
|
56
|
+
}}
|
|
57
|
+
onPress={item.onPress}
|
|
58
|
+
className="h-12 w-12 items-center justify-center rounded-xl bg-accent hover:bg-accent/80 transition-colors"
|
|
59
|
+
>
|
|
60
|
+
{item.icon}
|
|
61
|
+
</AnimatedPressable>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, ScrollView, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { FadeUp } from '../motion/fade-up';
|
|
5
|
+
|
|
6
|
+
export interface MasonryGridProps extends ViewProps {
|
|
7
|
+
data: any[];
|
|
8
|
+
renderItem: (item: any, index: number) => React.ReactNode;
|
|
9
|
+
columns?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MasonryGrid = React.forwardRef<React.ElementRef<typeof ScrollView>, MasonryGridProps>(
|
|
13
|
+
({ className, data, renderItem, columns = 2, ...props }, ref) => {
|
|
14
|
+
|
|
15
|
+
// Distribute items into columns
|
|
16
|
+
const columnsData = Array.from({ length: columns }, () => [] as any[]);
|
|
17
|
+
data.forEach((item, i) => {
|
|
18
|
+
const col = columnsData[i % columns];
|
|
19
|
+
if (col) {
|
|
20
|
+
col.push({ item, index: i });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ScrollView ref={ref as any} className={cn('flex-1', className)} {...props}>
|
|
26
|
+
<View className="flex-row items-start px-2 py-4 gap-2">
|
|
27
|
+
{columnsData.map((col, colIndex) => (
|
|
28
|
+
<View key={colIndex} className="flex-1 flex-col gap-2">
|
|
29
|
+
{col.map((wrapper) => (
|
|
30
|
+
<FadeUp key={wrapper.index} delay={wrapper.index * 50}>
|
|
31
|
+
{renderItem(wrapper.item, wrapper.index)}
|
|
32
|
+
</FadeUp>
|
|
33
|
+
))}
|
|
34
|
+
</View>
|
|
35
|
+
))}
|
|
36
|
+
</View>
|
|
37
|
+
</ScrollView>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
MasonryGrid.displayName = 'MasonryGrid';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, Dimensions } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedScrollHandler,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
interpolate,
|
|
8
|
+
Extrapolate,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import { cn } from '../../lib/utils';
|
|
11
|
+
|
|
12
|
+
export interface ParallaxScrollProps {
|
|
13
|
+
headerImage: React.ReactNode;
|
|
14
|
+
headerHeight?: number;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
20
|
+
|
|
21
|
+
export const ParallaxScroll = React.forwardRef<React.ElementRef<typeof Animated.ScrollView>, ParallaxScrollProps>(
|
|
22
|
+
({ className, headerImage, headerHeight = 300, children, ...props }, ref) => {
|
|
23
|
+
const scrollY = useSharedValue(0);
|
|
24
|
+
|
|
25
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
26
|
+
onScroll: (event) => {
|
|
27
|
+
scrollY.value = event.contentOffset.y;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const headerAnimatedStyle = useAnimatedStyle(() => {
|
|
32
|
+
const translateY = interpolate(
|
|
33
|
+
scrollY.value,
|
|
34
|
+
[-headerHeight, 0, headerHeight],
|
|
35
|
+
[-headerHeight / 2, 0, headerHeight * 0.5],
|
|
36
|
+
Extrapolate.CLAMP
|
|
37
|
+
);
|
|
38
|
+
const scale = interpolate(
|
|
39
|
+
scrollY.value,
|
|
40
|
+
[-headerHeight, 0],
|
|
41
|
+
[2, 1],
|
|
42
|
+
Extrapolate.CLAMP
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
transform: [{ translateY }, { scale }],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View className={cn('flex-1 bg-background', className)}>
|
|
52
|
+
<Animated.View style={[styles.header, { height: headerHeight }, headerAnimatedStyle]}>
|
|
53
|
+
{headerImage}
|
|
54
|
+
</Animated.View>
|
|
55
|
+
<Animated.ScrollView
|
|
56
|
+
ref={ref as any}
|
|
57
|
+
onScroll={scrollHandler}
|
|
58
|
+
scrollEventThrottle={16}
|
|
59
|
+
contentContainerStyle={{ paddingTop: headerHeight }}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
<View className="bg-background min-h-screen rounded-t-3xl -mt-6 p-6">
|
|
63
|
+
{children}
|
|
64
|
+
</View>
|
|
65
|
+
</Animated.ScrollView>
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
ParallaxScroll.displayName = 'ParallaxScroll';
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
header: {
|
|
74
|
+
position: 'absolute',
|
|
75
|
+
top: 0,
|
|
76
|
+
left: 0,
|
|
77
|
+
right: 0,
|
|
78
|
+
width: SCREEN_WIDTH,
|
|
79
|
+
overflow: 'hidden',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps, type TextStyle } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withSpring,
|
|
7
|
+
interpolate,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
10
|
+
import { cn } from '../../lib/utils';
|
|
11
|
+
import { Text } from '../../components/typography';
|
|
12
|
+
|
|
13
|
+
export interface AnimatedNumberProps extends ViewProps {
|
|
14
|
+
value: number;
|
|
15
|
+
fontSize?: number;
|
|
16
|
+
textStyle?: TextStyle | string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NUMBERS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renders a single digit column that slides up/down
|
|
23
|
+
*/
|
|
24
|
+
const AnimatedDigit = ({ digit, fontSize = 32, textStyle }: { digit: number, fontSize?: number, textStyle?: any }) => {
|
|
25
|
+
const translateY = useSharedValue(0);
|
|
26
|
+
const springConfig = useSpring('bouncy');
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
translateY.value = withSpring(-digit * fontSize, springConfig);
|
|
30
|
+
}, [digit, fontSize, springConfig]);
|
|
31
|
+
|
|
32
|
+
const rStyle = useAnimatedStyle(() => {
|
|
33
|
+
return {
|
|
34
|
+
transform: [{ translateY: translateY.value }],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={{ height: fontSize, overflow: 'hidden' }}>
|
|
40
|
+
<Animated.View style={rStyle}>
|
|
41
|
+
{NUMBERS.map((num) => (
|
|
42
|
+
<Text
|
|
43
|
+
key={num}
|
|
44
|
+
style={[{ height: fontSize, lineHeight: fontSize, fontSize }, typeof textStyle === 'object' ? textStyle : {}]}
|
|
45
|
+
className={cn('text-center font-bold font-mono tracking-tighter', typeof textStyle === 'string' ? textStyle : '')}
|
|
46
|
+
>
|
|
47
|
+
{num}
|
|
48
|
+
</Text>
|
|
49
|
+
))}
|
|
50
|
+
</Animated.View>
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A Magic UI inspired Animated Number component.
|
|
57
|
+
* Rolls the digits into place like a slot machine when the value changes.
|
|
58
|
+
*/
|
|
59
|
+
export const AnimatedNumber = React.forwardRef<React.ElementRef<typeof View>, AnimatedNumberProps>(
|
|
60
|
+
({ value, fontSize = 48, textStyle, className, style, ...props }, ref) => {
|
|
61
|
+
const valueStr = Math.abs(value).toString();
|
|
62
|
+
const isNegative = value < 0;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={cn('flex-row items-center overflow-hidden', className)}
|
|
68
|
+
style={style}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{isNegative && (
|
|
72
|
+
<Text
|
|
73
|
+
style={[{ fontSize, lineHeight: fontSize }, typeof textStyle === 'object' ? textStyle : {}]}
|
|
74
|
+
className={cn('font-bold font-mono tracking-tighter', typeof textStyle === 'string' ? textStyle : '')}
|
|
75
|
+
>
|
|
76
|
+
-
|
|
77
|
+
</Text>
|
|
78
|
+
)}
|
|
79
|
+
{valueStr.split('').map((char, index) => {
|
|
80
|
+
if (char === '.') {
|
|
81
|
+
return (
|
|
82
|
+
<Text
|
|
83
|
+
key={`dot-${index}`}
|
|
84
|
+
style={[{ fontSize, lineHeight: fontSize }, typeof textStyle === 'object' ? textStyle : {}]}
|
|
85
|
+
className={cn('font-bold font-mono tracking-tighter', typeof textStyle === 'string' ? textStyle : '')}
|
|
86
|
+
>
|
|
87
|
+
.
|
|
88
|
+
</Text>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<AnimatedDigit
|
|
93
|
+
key={`${index}-${valueStr.length}`}
|
|
94
|
+
digit={parseInt(char, 10)}
|
|
95
|
+
fontSize={fontSize}
|
|
96
|
+
textStyle={textStyle}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
AnimatedNumber.displayName = 'AnimatedNumber';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
export interface BentoGridProps extends ViewProps {}
|
|
6
|
+
|
|
7
|
+
export const BentoGrid = React.forwardRef<React.ElementRef<typeof View>, BentoGridProps>(
|
|
8
|
+
({ className, children, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<View
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn(
|
|
13
|
+
'flex flex-row flex-wrap gap-4 mx-auto w-full max-w-7xl',
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
BentoGrid.displayName = 'BentoGrid';
|
|
24
|
+
|
|
25
|
+
export interface BentoCardProps extends ViewProps {
|
|
26
|
+
colSpan?: 1 | 2 | 3 | 4;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const BentoCard = React.forwardRef<React.ElementRef<typeof View>, BentoCardProps>(
|
|
30
|
+
({ className, colSpan = 1, children, ...props }, ref) => {
|
|
31
|
+
// Very basic mapping for flex-basis based on a 4-column desktop grid
|
|
32
|
+
// For React Native, percentage widths work best. We'll subtract the gap roughly.
|
|
33
|
+
const basisMap = {
|
|
34
|
+
1: 'basis-[23%]',
|
|
35
|
+
2: 'basis-[48%]',
|
|
36
|
+
3: 'basis-[73%]',
|
|
37
|
+
4: 'basis-[100%]',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
'flex-1 min-w-[280px] rounded-xl border border-border bg-card shadow-sm overflow-hidden p-6',
|
|
45
|
+
basisMap[colSpan],
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
BentoCard.displayName = 'BentoCard';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet, type ViewProps, Dimensions } 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 BorderBeamProps extends ViewProps {
|
|
14
|
+
duration?: number;
|
|
15
|
+
color?: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const BorderBeam = React.forwardRef<React.ElementRef<typeof View>, BorderBeamProps>(
|
|
20
|
+
({ className, duration = 4000, color = 'hsl(var(--primary))', size = 50, style, ...props }, ref) => {
|
|
21
|
+
const perimeter = useSharedValue(0);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
perimeter.value = withRepeat(
|
|
25
|
+
withTiming(1, { duration, easing: Easing.linear }),
|
|
26
|
+
-1,
|
|
27
|
+
false
|
|
28
|
+
);
|
|
29
|
+
}, [duration]);
|
|
30
|
+
|
|
31
|
+
// This is a simplified border beam using a rotating gradient mask approach
|
|
32
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
33
|
+
transform: [{ rotate: `${interpolate(perimeter.value, [0, 1], [0, 360])}deg` }],
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
ref={ref}
|
|
39
|
+
className="absolute inset-0 z-10 overflow-hidden rounded-[inherit] pointer-events-none"
|
|
40
|
+
style={style}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<Animated.View
|
|
44
|
+
style={[
|
|
45
|
+
{
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: '-50%',
|
|
48
|
+
left: '-50%',
|
|
49
|
+
width: '200%',
|
|
50
|
+
height: '200%',
|
|
51
|
+
},
|
|
52
|
+
animatedStyle,
|
|
53
|
+
]}
|
|
54
|
+
>
|
|
55
|
+
{/* The beam is a conic/linear sweep. Linear works reasonably well when masked by borders. */}
|
|
56
|
+
<LinearGradient
|
|
57
|
+
colors={[color, 'transparent', 'transparent']}
|
|
58
|
+
start={{ x: 0.5, y: 0.5 }}
|
|
59
|
+
end={{ x: 1, y: 1 }}
|
|
60
|
+
style={StyleSheet.absoluteFill}
|
|
61
|
+
/>
|
|
62
|
+
</Animated.View>
|
|
63
|
+
<View className="absolute inset-[2px] rounded-[inherit] bg-background" />
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
BorderBeam.displayName = 'BorderBeam';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, StyleSheet, Dimensions } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
withDelay,
|
|
8
|
+
Easing,
|
|
9
|
+
runOnJS,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
|
|
12
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
13
|
+
const COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#a855f7'];
|
|
14
|
+
|
|
15
|
+
export const Confetti = ({ count = 50, duration = 3000, onComplete }: { count?: number; duration?: number; onComplete?: () => void }) => {
|
|
16
|
+
const [pieces, setPieces] = useState<any[]>([]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const newPieces = Array.from({ length: count }).map((_, i) => ({
|
|
20
|
+
id: i,
|
|
21
|
+
x: Math.random() * SCREEN_WIDTH,
|
|
22
|
+
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
|
23
|
+
delay: Math.random() * 500,
|
|
24
|
+
size: Math.random() * 10 + 5,
|
|
25
|
+
}));
|
|
26
|
+
setPieces(newPieces);
|
|
27
|
+
}, [count]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
31
|
+
{pieces.map((p, i) => (
|
|
32
|
+
<ConfettiPiece
|
|
33
|
+
key={p.id}
|
|
34
|
+
x={p.x}
|
|
35
|
+
color={p.color}
|
|
36
|
+
delay={p.delay}
|
|
37
|
+
size={p.size}
|
|
38
|
+
duration={duration}
|
|
39
|
+
onComplete={i === pieces.length - 1 ? onComplete : undefined}
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ConfettiPiece = ({ x, color, delay, size, duration, onComplete }: any) => {
|
|
47
|
+
const translateY = useSharedValue(-50);
|
|
48
|
+
const rotateX = useSharedValue(0);
|
|
49
|
+
const rotateY = useSharedValue(0);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
translateY.value = withDelay(
|
|
53
|
+
delay,
|
|
54
|
+
withTiming(SCREEN_HEIGHT + 50, { duration, easing: Easing.linear }, (finished) => {
|
|
55
|
+
if (finished && onComplete) {
|
|
56
|
+
runOnJS(onComplete)();
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
rotateX.value = withDelay(delay, withTiming(Math.random() * 720, { duration, easing: Easing.linear }));
|
|
62
|
+
rotateY.value = withDelay(delay, withTiming(Math.random() * 720, { duration, easing: Easing.linear }));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
66
|
+
transform: [
|
|
67
|
+
{ translateY: translateY.value },
|
|
68
|
+
{ rotateX: `${rotateX.value}deg` },
|
|
69
|
+
{ rotateY: `${rotateY.value}deg` },
|
|
70
|
+
],
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Animated.View
|
|
75
|
+
style={[
|
|
76
|
+
{
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
top: 0,
|
|
79
|
+
left: x,
|
|
80
|
+
width: size,
|
|
81
|
+
height: size,
|
|
82
|
+
backgroundColor: color,
|
|
83
|
+
},
|
|
84
|
+
animatedStyle,
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
interpolateColor,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
12
|
+
import { cn } from '../../lib/utils';
|
|
13
|
+
|
|
14
|
+
export interface MagicCardProps extends ViewProps {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MagicCard = React.forwardRef<React.ElementRef<typeof View>, MagicCardProps>(
|
|
19
|
+
({ className, children, style, ...props }, ref) => {
|
|
20
|
+
const rotation = useSharedValue(0);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
rotation.value = withRepeat(
|
|
24
|
+
withTiming(360, { duration: 4000, easing: Easing.linear }),
|
|
25
|
+
-1,
|
|
26
|
+
false
|
|
27
|
+
);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
31
|
+
transform: [{ rotate: `${rotation.value}deg` }],
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn('relative overflow-hidden rounded-2xl bg-card p-[1px]', className)}
|
|
38
|
+
style={style}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{/* The rotating gradient background behind the inner card */}
|
|
42
|
+
<Animated.View
|
|
43
|
+
style={[
|
|
44
|
+
StyleSheet.absoluteFill,
|
|
45
|
+
{ width: '200%', height: '200%', top: '-50%', left: '-50%' },
|
|
46
|
+
animatedStyle,
|
|
47
|
+
]}
|
|
48
|
+
>
|
|
49
|
+
<LinearGradient
|
|
50
|
+
colors={['transparent', 'hsl(var(--primary))', 'transparent']}
|
|
51
|
+
start={{ x: 0, y: 0 }}
|
|
52
|
+
end={{ x: 1, y: 1 }}
|
|
53
|
+
style={StyleSheet.absoluteFill}
|
|
54
|
+
/>
|
|
55
|
+
</Animated.View>
|
|
56
|
+
|
|
57
|
+
{/* Inner Card content */}
|
|
58
|
+
<View className="relative h-full w-full rounded-[15px] bg-card p-6">
|
|
59
|
+
{children}
|
|
60
|
+
</View>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
MagicCard.displayName = 'MagicCard';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, type ViewProps, Dimensions } 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 { cn } from '../../lib/utils';
|
|
13
|
+
|
|
14
|
+
export interface MeteorsProps extends ViewProps {
|
|
15
|
+
number?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
19
|
+
|
|
20
|
+
export const Meteors = React.forwardRef<React.ElementRef<typeof View>, MeteorsProps>(
|
|
21
|
+
({ className, number = 20, style, ...props }, ref) => {
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn('absolute inset-0 overflow-hidden', className)}
|
|
27
|
+
style={style}
|
|
28
|
+
pointerEvents="none"
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{Array.from({ length: number }).map((_, idx) => (
|
|
32
|
+
<Meteor key={idx} />
|
|
33
|
+
))}
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
Meteors.displayName = 'Meteors';
|
|
39
|
+
|
|
40
|
+
const Meteor = () => {
|
|
41
|
+
const animatedValue = useSharedValue(0);
|
|
42
|
+
|
|
43
|
+
// Randomize initial properties
|
|
44
|
+
const top = Math.random() * SCREEN_HEIGHT * 0.5 - 100;
|
|
45
|
+
const left = Math.random() * SCREEN_WIDTH * 1.5 - SCREEN_WIDTH * 0.2;
|
|
46
|
+
const delay = Math.random() * 5000;
|
|
47
|
+
const duration = Math.random() * 2000 + 1500;
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
animatedValue.value = withRepeat(
|
|
52
|
+
withTiming(1, { duration, easing: Easing.linear }),
|
|
53
|
+
-1,
|
|
54
|
+
false
|
|
55
|
+
);
|
|
56
|
+
}, delay);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
60
|
+
const translateX = interpolate(animatedValue.value, [0, 1], [0, -SCREEN_WIDTH * 1.5]);
|
|
61
|
+
const translateY = interpolate(animatedValue.value, [0, 1], [0, SCREEN_WIDTH * 1.5]);
|
|
62
|
+
const opacity = interpolate(animatedValue.value, [0, 0.1, 0.8, 1], [0, 1, 1, 0]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
transform: [
|
|
66
|
+
{ translateX },
|
|
67
|
+
{ translateY },
|
|
68
|
+
{ rotate: '-45deg' },
|
|
69
|
+
],
|
|
70
|
+
opacity,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Animated.View
|
|
76
|
+
style={[
|
|
77
|
+
{
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
top,
|
|
80
|
+
left,
|
|
81
|
+
width: 100,
|
|
82
|
+
height: 2,
|
|
83
|
+
},
|
|
84
|
+
animatedStyle,
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
<LinearGradient
|
|
88
|
+
colors={['rgba(255,255,255,0.8)', 'transparent']}
|
|
89
|
+
start={{ x: 0, y: 0 }}
|
|
90
|
+
end={{ x: 1, y: 0 }}
|
|
91
|
+
style={StyleSheet.absoluteFill}
|
|
92
|
+
/>
|
|
93
|
+
</Animated.View>
|
|
94
|
+
);
|
|
95
|
+
};
|