@munchi_oy/native-ui 0.1.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 (83) hide show
  1. package/dist/index.d.mts +568 -0
  2. package/dist/index.d.ts +568 -0
  3. package/dist/index.js +1 -0
  4. package/dist/index.mjs +1 -0
  5. package/global.css +53 -0
  6. package/nativewind-env.d.ts +2 -0
  7. package/package.json +88 -0
  8. package/src/MAlert.tsx +38 -0
  9. package/src/MAnimation.tsx +55 -0
  10. package/src/MAvatar.tsx +111 -0
  11. package/src/MBadge.tsx +72 -0
  12. package/src/MButton.tsx +90 -0
  13. package/src/MCard.tsx +15 -0
  14. package/src/MChevron.tsx +47 -0
  15. package/src/MConfirmation.tsx +68 -0
  16. package/src/MCountDown.tsx +120 -0
  17. package/src/MDateTimePicker.tsx +124 -0
  18. package/src/MDivider.tsx +69 -0
  19. package/src/MDrawerRightPanel.tsx +187 -0
  20. package/src/MDropdown.tsx +277 -0
  21. package/src/MInput.tsx +162 -0
  22. package/src/MLabel.tsx +3 -0
  23. package/src/MLucideIcon.tsx +21 -0
  24. package/src/MModal.tsx +287 -0
  25. package/src/MNativeAlert.tsx +33 -0
  26. package/src/MNumpad.tsx +520 -0
  27. package/src/MPicker.tsx +150 -0
  28. package/src/MPinPadKeys.tsx +104 -0
  29. package/src/MPortal.tsx +4 -0
  30. package/src/MProgressBar.tsx +74 -0
  31. package/src/MRadioGroup.tsx +4 -0
  32. package/src/MRequiredLabel.tsx +21 -0
  33. package/src/MResponsiveContainer.tsx +74 -0
  34. package/src/MSearch.tsx +138 -0
  35. package/src/MSelector.tsx +48 -0
  36. package/src/MSkeleton.tsx +3 -0
  37. package/src/MSwitch.tsx +13 -0
  38. package/src/MTable.tsx +17 -0
  39. package/src/MTabs.tsx +198 -0
  40. package/src/MText.tsx +51 -0
  41. package/src/MTimerUp.tsx +88 -0
  42. package/src/MToggle.tsx +51 -0
  43. package/src/constants.ts +19 -0
  44. package/src/hooks/useColorScheme.tsx +12 -0
  45. package/src/hooks/useIconColors.ts +19 -0
  46. package/src/index.ts +124 -0
  47. package/src/primitives/accordion.tsx +143 -0
  48. package/src/primitives/alert-dialog.tsx +181 -0
  49. package/src/primitives/alert.tsx +94 -0
  50. package/src/primitives/aspect-ratio.tsx +5 -0
  51. package/src/primitives/avatar.tsx +47 -0
  52. package/src/primitives/badge.tsx +57 -0
  53. package/src/primitives/button.tsx +92 -0
  54. package/src/primitives/card.tsx +86 -0
  55. package/src/primitives/checkbox.tsx +35 -0
  56. package/src/primitives/collapsible.tsx +9 -0
  57. package/src/primitives/context-menu.tsx +255 -0
  58. package/src/primitives/dialog.tsx +166 -0
  59. package/src/primitives/dropdown-menu.tsx +264 -0
  60. package/src/primitives/hover-card.tsx +45 -0
  61. package/src/primitives/input.tsx +25 -0
  62. package/src/primitives/label.tsx +33 -0
  63. package/src/primitives/menubar.tsx +266 -0
  64. package/src/primitives/navigation-menu.tsx +192 -0
  65. package/src/primitives/popover.tsx +46 -0
  66. package/src/primitives/progress.tsx +82 -0
  67. package/src/primitives/radio-group.tsx +42 -0
  68. package/src/primitives/select.tsx +192 -0
  69. package/src/primitives/separator.tsx +28 -0
  70. package/src/primitives/skeleton.tsx +39 -0
  71. package/src/primitives/switch.tsx +102 -0
  72. package/src/primitives/table.tsx +107 -0
  73. package/src/primitives/tabs.tsx +66 -0
  74. package/src/primitives/text.tsx +28 -0
  75. package/src/primitives/textarea.tsx +39 -0
  76. package/src/primitives/toggle-group.tsx +89 -0
  77. package/src/primitives/toggle.tsx +91 -0
  78. package/src/primitives/tooltip.tsx +40 -0
  79. package/src/primitives/typography.tsx +214 -0
  80. package/src/theme.ts +43 -0
  81. package/src/tokens.ts +7 -0
  82. package/src/utils.ts +14 -0
  83. package/tailwind.config.ts +112 -0
@@ -0,0 +1,104 @@
1
+ import { Delete } from "lucide-react-native";
2
+ import type React from "react";
3
+ import { Pressable, View } from "react-native";
4
+ import { MText } from "./MText";
5
+ import { useIconColors } from "./hooks/useIconColors";
6
+ import { cn } from "./utils";
7
+
8
+ type KeyProps = {
9
+ label?: string;
10
+ onPress: () => void;
11
+ testID?: string;
12
+ icon?: React.ReactNode;
13
+ disabled: boolean;
14
+ iconTint: string;
15
+ labelClassName: string;
16
+ };
17
+
18
+ const PinPadKey: React.FC<KeyProps> = ({
19
+ label,
20
+ onPress,
21
+ testID,
22
+ icon,
23
+ disabled,
24
+ iconTint,
25
+ labelClassName
26
+ }) => {
27
+ return (
28
+ <Pressable
29
+ testID={testID}
30
+ onPress={onPress}
31
+ className="w-20 h-20 md:w-24 md:h-24 rounded-full items-center justify-center active:bg-accent"
32
+ disabled={disabled}
33
+ >
34
+ {label ? (
35
+ <MText
36
+ className={cn("text-3xl md:text-4xl font-light", labelClassName)}
37
+ >
38
+ {label}
39
+ </MText>
40
+ ) : (
41
+ (icon ?? <Delete size={26} color={iconTint} />)
42
+ )}
43
+ </Pressable>
44
+ );
45
+ };
46
+
47
+ export interface MPinPadKeysProps {
48
+ onDigit: (d: string) => void;
49
+ onBackspace: () => void;
50
+ isLoading: boolean;
51
+ }
52
+
53
+ export const MPinPadKeys: React.FC<MPinPadKeysProps> = ({
54
+ onDigit,
55
+ onBackspace,
56
+ isLoading
57
+ }) => {
58
+ const { foreground } = useIconColors();
59
+
60
+ const keyRows = [
61
+ ["1", "2", "3"],
62
+ ["4", "5", "6"],
63
+ ["7", "8", "9"]
64
+ ];
65
+
66
+ return (
67
+ <View className="w-full items-center gap-0">
68
+ {keyRows.map((row, rowIndex) => (
69
+ <View key={rowIndex} className="flex-row justify-center gap-24">
70
+ {row.map((digit) => (
71
+ <PinPadKey
72
+ key={digit}
73
+ label={digit}
74
+ onPress={() => onDigit(digit)}
75
+ disabled={isLoading}
76
+ iconTint={foreground}
77
+ labelClassName="text-foreground"
78
+ />
79
+ ))}
80
+ </View>
81
+ ))}
82
+ <View className="flex-row justify-center gap-24">
83
+ <View className="w-20 h-20 md:w-24 md:h-24" />
84
+ <PinPadKey
85
+ label="0"
86
+ onPress={() => onDigit("0")}
87
+ disabled={isLoading}
88
+ iconTint={foreground}
89
+ labelClassName="text-foreground"
90
+ />
91
+ <PinPadKey
92
+ onPress={onBackspace}
93
+ testID="backspace"
94
+ disabled={isLoading}
95
+ iconTint={foreground}
96
+ labelClassName="text-foreground"
97
+ icon={<Delete size={26} color={foreground} />}
98
+ />
99
+ </View>
100
+ </View>
101
+ );
102
+ };
103
+
104
+ MPinPadKeys.displayName = "MPinPadKeys";
@@ -0,0 +1,4 @@
1
+ import { Portal, PortalHost } from "@rn-primitives/portal";
2
+
3
+ export { Portal as MPortal };
4
+ export { PortalHost as MPortalHost };
@@ -0,0 +1,74 @@
1
+ import { type VariantProps, cva } from "class-variance-authority";
2
+ import type React from "react";
3
+ import { View, type ViewProps } from "react-native";
4
+ import { cn } from "./utils";
5
+
6
+ const progressBarVariants = cva("w-full h-2 rounded-full relative", {
7
+ variants: {
8
+ size: {
9
+ sm: "h-1",
10
+ md: "h-2",
11
+ lg: "h-3"
12
+ },
13
+ variant: {
14
+ default: "bg-muted",
15
+ accent: "bg-accent/30",
16
+ destructive: "bg-destructive/30",
17
+ secondary: "bg-secondary",
18
+ gray: "bg-gray",
19
+ pink: "bg-pink",
20
+ blue: "bg-blue-200",
21
+ green: "bg-green-200"
22
+ }
23
+ },
24
+ defaultVariants: {
25
+ size: "md",
26
+ variant: "default"
27
+ }
28
+ });
29
+
30
+ const progressFillVariants = cva("h-full rounded-full absolute top-0 left-0", {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-foreground",
34
+ accent: "bg-accent",
35
+ destructive: "bg-destructive",
36
+ secondary: "bg-secondary-foreground",
37
+ gray: "bg-gray-600",
38
+ pink: "bg-pink",
39
+ blue: "bg-blue-600",
40
+ green: "bg-green-600"
41
+ }
42
+ },
43
+ defaultVariants: {
44
+ variant: "default"
45
+ }
46
+ });
47
+
48
+ export interface MProgressBarProps
49
+ extends Pick<ViewProps, "className">,
50
+ VariantProps<typeof progressBarVariants> {
51
+ progress: number;
52
+ fillClassName?: ViewProps["className"];
53
+ }
54
+
55
+ export const MProgressBar: React.FC<MProgressBarProps> = ({
56
+ progress,
57
+ size = "md",
58
+ variant = "default",
59
+ className,
60
+ fillClassName
61
+ }) => {
62
+ const clampedProgress = Math.min(Math.max(progress, 0), 100);
63
+
64
+ return (
65
+ <View className={cn(progressBarVariants({ size, variant }), className)}>
66
+ <View
67
+ style={{ width: `${clampedProgress}%` }}
68
+ className={cn(progressFillVariants({ variant }), fillClassName)}
69
+ />
70
+ </View>
71
+ );
72
+ };
73
+
74
+ MProgressBar.displayName = "MProgressBar";
@@ -0,0 +1,4 @@
1
+ import { RadioGroup, RadioGroupItem } from "./primitives/radio-group";
2
+
3
+ export { RadioGroup as MRadioGroup };
4
+ export { RadioGroupItem as MRadioGroupItem };
@@ -0,0 +1,21 @@
1
+ import { View, type ViewProps } from "react-native";
2
+ import { MText } from "./MText";
3
+ import { cn } from "./utils";
4
+
5
+ type MRequiredLabelProps = ViewProps & {
6
+ label: string;
7
+ };
8
+
9
+ export const MRequiredLabel = ({
10
+ label,
11
+ className,
12
+ ...props
13
+ }: MRequiredLabelProps) => (
14
+ <View
15
+ className={cn("flex-row items-center gap-1 mb-3", className)}
16
+ {...props}
17
+ >
18
+ <MText className="font-munchi-bold text-xl">{label}</MText>
19
+ <MText className="font-munchi-bold text-xl text-destructive">*</MText>
20
+ </View>
21
+ );
@@ -0,0 +1,74 @@
1
+ import type React from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { cn } from "./utils";
4
+
5
+ interface MResponsiveContainerProps extends Omit<ViewProps, "children"> {
6
+ children: React.ReactNode;
7
+ /**
8
+ * Responsive padding values. Keys are Tailwind breakpoints; values must be
9
+ * valid Tailwind spacing scale steps (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, …).
10
+ */
11
+ padding?: {
12
+ xs?: number;
13
+ sm?: number;
14
+ md?: number;
15
+ lg?: number;
16
+ xl?: number;
17
+ "2xl"?: number;
18
+ };
19
+ /**
20
+ * Responsive margin values. Keys are Tailwind breakpoints; values must be
21
+ * valid Tailwind spacing scale steps (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, …).
22
+ */
23
+ margin?: {
24
+ xs?: number;
25
+ sm?: number;
26
+ md?: number;
27
+ lg?: number;
28
+ xl?: number;
29
+ "2xl"?: number;
30
+ };
31
+ }
32
+
33
+ export const MResponsiveContainer: React.FC<MResponsiveContainerProps> = ({
34
+ children,
35
+ className,
36
+ style,
37
+ padding,
38
+ margin,
39
+ ...rest
40
+ }) => {
41
+ const getResponsiveClasses = () => {
42
+ const classes: string[] = [];
43
+
44
+ if (padding) {
45
+ for (const [size, value] of Object.entries(padding)) {
46
+ if (value !== undefined) {
47
+ classes.push(`${size}:p-${value}`);
48
+ }
49
+ }
50
+ }
51
+
52
+ if (margin) {
53
+ for (const [size, value] of Object.entries(margin)) {
54
+ if (value !== undefined) {
55
+ classes.push(`${size}:m-${value}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ return classes.join(" ");
61
+ };
62
+
63
+ return (
64
+ <View
65
+ className={cn(getResponsiveClasses(), className)}
66
+ style={style}
67
+ {...rest}
68
+ >
69
+ {children}
70
+ </View>
71
+ );
72
+ };
73
+
74
+ MResponsiveContainer.displayName = "MResponsiveContainer";
@@ -0,0 +1,138 @@
1
+ import { type VariantProps, cva } from "class-variance-authority";
2
+ import { Search, X } from "lucide-react-native";
3
+ import React from "react";
4
+ import {
5
+ Keyboard,
6
+ Pressable,
7
+ TextInput,
8
+ type TextInputProps,
9
+ type TextProps,
10
+ View,
11
+ type ViewProps
12
+ } from "react-native";
13
+ import { useIconColors } from "./hooks/useIconColors";
14
+ import { cn } from "./utils";
15
+
16
+ const containerVariants = cva(
17
+ "flex-row items-center bg-card rounded-full border border-border",
18
+ {
19
+ variants: {
20
+ size: {
21
+ default: "",
22
+ sm: "",
23
+ lg: ""
24
+ }
25
+ },
26
+ defaultVariants: {
27
+ size: "default"
28
+ }
29
+ }
30
+ );
31
+
32
+ const INPUT_SIZE: Record<string, number> = {
33
+ default: 18,
34
+ sm: 15,
35
+ lg: 21
36
+ };
37
+
38
+ const INPUT_HEIGHT: Record<string, number> = {
39
+ default: 36,
40
+ sm: 30,
41
+ lg: 52
42
+ };
43
+
44
+ const inputVariants = cva("flex-1 min-w-0 px-2 font-munchi", {
45
+ variants: {
46
+ size: {
47
+ default: "text-lg",
48
+ sm: "text-base",
49
+ lg: "text-xl"
50
+ }
51
+ },
52
+ defaultVariants: {
53
+ size: "default"
54
+ }
55
+ });
56
+
57
+ export interface MSearchProps
58
+ extends Omit<TextInputProps, "className">,
59
+ VariantProps<typeof containerVariants> {
60
+ value: string;
61
+ onChangeText: (text: string) => void;
62
+ onClear?: () => void;
63
+ placeholder?: string;
64
+ className?: ViewProps["className"];
65
+ containerClassName?: ViewProps["className"];
66
+ inputClassName?: TextProps["className"];
67
+ iconSize?: number;
68
+ }
69
+
70
+ export const MSearch = React.forwardRef<TextInput, MSearchProps>(
71
+ (
72
+ {
73
+ value,
74
+ onChangeText,
75
+ onClear,
76
+ placeholder = "Search...",
77
+ size = "default",
78
+ className,
79
+ containerClassName,
80
+ inputClassName,
81
+ iconSize = 20,
82
+ placeholderTextColor: placeholderTextColorProp,
83
+ ...textInputProps
84
+ },
85
+ ref
86
+ ) => {
87
+ const { foreground, muted } = useIconColors();
88
+
89
+ const handleClear = () => {
90
+ if (onClear) onClear();
91
+ else onChangeText("");
92
+ if (Keyboard.isVisible()) Keyboard.dismiss();
93
+ };
94
+
95
+ return (
96
+ <View
97
+ className={cn(
98
+ containerVariants({ size }),
99
+ className,
100
+ containerClassName
101
+ )}
102
+ >
103
+ <View className="pl-3 shrink-0">
104
+ <Search size={iconSize} color={muted} />
105
+ </View>
106
+ <TextInput
107
+ ref={ref}
108
+ placeholder={placeholder}
109
+ placeholderTextColor={placeholderTextColorProp ?? muted}
110
+ value={value}
111
+ onChangeText={onChangeText}
112
+ textAlignVertical="center"
113
+ style={{
114
+ color: foreground,
115
+ fontSize: INPUT_SIZE[size ?? "default"],
116
+ height: INPUT_HEIGHT[size ?? "default"],
117
+ paddingVertical: 0,
118
+ includeFontPadding: false
119
+ }}
120
+ className={cn(inputVariants({ size }), inputClassName)}
121
+ {...textInputProps}
122
+ />
123
+ <View className="pr-2 shrink-0" style={{ width: iconSize + 16 }}>
124
+ {value.length > 0 && (
125
+ <Pressable
126
+ onPress={handleClear}
127
+ className="items-center justify-center p-2"
128
+ >
129
+ <X size={iconSize} color={muted} />
130
+ </Pressable>
131
+ )}
132
+ </View>
133
+ </View>
134
+ );
135
+ }
136
+ );
137
+
138
+ MSearch.displayName = "MSearch";
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { Pressable, Text, View } from "react-native";
3
+ import { cn } from "./utils";
4
+
5
+ export interface MSelectorProps {
6
+ label: string;
7
+ isSelected: boolean;
8
+ onPress: () => void;
9
+ description?: string | null;
10
+ }
11
+
12
+ export const MSelector = React.memo(
13
+ ({ label, isSelected, onPress, description }: MSelectorProps) => {
14
+ return (
15
+ <Pressable
16
+ className="flex flex-row justify-between items-center p-2"
17
+ onPress={onPress}
18
+ >
19
+ <Text
20
+ className={cn("font-munchi", isSelected && "font-munchi-semibold")}
21
+ >
22
+ {label}
23
+ </Text>
24
+
25
+ {isSelected && description && (
26
+ <Text className="text-muted-foreground text-sm mx-2 flex-1 text-right">
27
+ {description}
28
+ </Text>
29
+ )}
30
+
31
+ <View
32
+ className={cn(
33
+ "w-6 h-6 rounded-full border-2 border-primary ml-2",
34
+ !isSelected && "bg-card"
35
+ )}
36
+ >
37
+ {isSelected && (
38
+ <View className="absolute inset-0 m-1 rounded-full bg-primary" />
39
+ )}
40
+ </View>
41
+ </Pressable>
42
+ );
43
+ }
44
+ );
45
+
46
+ MSelector.displayName = "MSelector";
47
+
48
+ export default MSelector;
@@ -0,0 +1,3 @@
1
+ import { Skeleton } from "./primitives/skeleton";
2
+
3
+ export { Skeleton as MSkeleton };
@@ -0,0 +1,13 @@
1
+ import { Switch } from "react-native";
2
+ import type { SwitchProps } from "react-native";
3
+
4
+ export type MSwitchProps = Omit<
5
+ SwitchProps,
6
+ "trackColor" | "thumbColor" | "ios_backgroundColor"
7
+ >;
8
+
9
+ export const MSwitch = (props: MSwitchProps) => {
10
+ return <Switch {...props} />;
11
+ };
12
+
13
+ MSwitch.displayName = "MSwitch";
package/src/MTable.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import {
2
+ Table,
3
+ TableBody,
4
+ TableCell,
5
+ TableFooter,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow
9
+ } from "./primitives/table";
10
+
11
+ export { Table as MTable };
12
+ export { TableBody as MTableBody };
13
+ export { TableCell as MTableCell };
14
+ export { TableFooter as MTableFooter };
15
+ export { TableHead as MTableHead };
16
+ export { TableHeader as MTableHeader };
17
+ export { TableRow as MTableRow };
package/src/MTabs.tsx ADDED
@@ -0,0 +1,198 @@
1
+ import type React from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import {
4
+ type LayoutRectangle,
5
+ Pressable,
6
+ View,
7
+ type ViewProps
8
+ } from "react-native";
9
+ import Animated, {
10
+ useAnimatedStyle,
11
+ useSharedValue,
12
+ withSpring
13
+ } from "react-native-reanimated";
14
+ import { MText } from "./MText";
15
+ import { NAV_THEME } from "./constants";
16
+ import { useColorScheme } from "./hooks/useColorScheme";
17
+ import { cn } from "./utils";
18
+
19
+ export interface Tab<T> {
20
+ key: T;
21
+ label?: string;
22
+ icon?: React.ReactNode;
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export interface MTabsProps<T> extends Omit<ViewProps, "children"> {
27
+ tabs: Tab<T>[];
28
+ activeTab: T | null;
29
+ onTabChange: (tab: T) => void;
30
+ tabClassName?: string;
31
+ activeTabClassName?: string;
32
+ disabled?: boolean;
33
+ size?: "default" | "compact";
34
+ variant?: "default" | "ghost";
35
+ stretch?: boolean;
36
+ }
37
+
38
+ const SPRING = { damping: 20, stiffness: 200, mass: 0.8 };
39
+
40
+ export const MTabs = <T,>({
41
+ tabs,
42
+ activeTab,
43
+ onTabChange,
44
+ className,
45
+ style,
46
+ disabled = false,
47
+ size = "default",
48
+ variant = "default",
49
+ stretch = false,
50
+ ...viewProps
51
+ }: MTabsProps<T>) => {
52
+ const { colorScheme } = useColorScheme();
53
+ const theme = NAV_THEME[colorScheme === "dark" ? "dark" : "light"];
54
+ const isCompact = size === "compact";
55
+ const isGhost = variant === "ghost";
56
+ const indicatorInset = isGhost ? 0 : isCompact ? 2 : 4;
57
+ const indicatorRadius = isGhost ? 20 : isCompact ? 8 : 12;
58
+ const pressPadH = isCompact ? 8 : 12;
59
+ const pressPadV = isGhost ? 7 : isCompact ? 2 : 4;
60
+ const pressRadius = isGhost ? 22 : isCompact ? 8 : 12;
61
+ const innerGap = isCompact ? 2 : 4;
62
+
63
+ const layouts = useRef<Record<string, LayoutRectangle>>({});
64
+ const indicatorX = useSharedValue(0);
65
+ const indicatorW = useSharedValue(0);
66
+ const initialized = useRef(false);
67
+
68
+ const [, forceUpdate] = useState(0);
69
+
70
+ useEffect(() => {
71
+ if (activeTab == null) {
72
+ initialized.current = false;
73
+ indicatorW.value = withSpring(0, SPRING);
74
+ return;
75
+ }
76
+ const keyStr = String(activeTab);
77
+ const rect = layouts.current[keyStr];
78
+ if (rect) {
79
+ indicatorX.value = withSpring(rect.x, SPRING);
80
+ indicatorW.value = withSpring(rect.width, SPRING);
81
+ initialized.current = true;
82
+ forceUpdate((n) => n + 1);
83
+ return;
84
+ }
85
+ initialized.current = false;
86
+ }, [activeTab, indicatorW, indicatorX]);
87
+
88
+ const indicatorStyle = useAnimatedStyle(() => ({
89
+ transform: [{ translateX: indicatorX.value }],
90
+ width: indicatorW.value
91
+ }));
92
+
93
+ const handleLayout = (key: string, rect: LayoutRectangle) => {
94
+ layouts.current[key] = rect;
95
+
96
+ const activeKey = String(activeTab);
97
+ if (key !== activeKey) return;
98
+
99
+ if (!initialized.current) {
100
+ indicatorX.value = rect.x;
101
+ indicatorW.value = rect.width;
102
+ initialized.current = true;
103
+ forceUpdate((n) => n + 1);
104
+ return;
105
+ }
106
+ indicatorX.value = withSpring(rect.x, SPRING);
107
+ indicatorW.value = withSpring(rect.width, SPRING);
108
+ };
109
+
110
+ const handleTabPress = (key: T) => {
111
+ const keyStr = String(key);
112
+ const rect = layouts.current[keyStr];
113
+ if (rect) {
114
+ indicatorX.value = withSpring(rect.x, SPRING);
115
+ indicatorW.value = withSpring(rect.width, SPRING);
116
+ }
117
+ onTabChange(key);
118
+ };
119
+
120
+ return (
121
+ <View
122
+ className={cn(
123
+ "flex-row",
124
+ !isGhost && "bg-accent",
125
+ isCompact ? "gap-0.5 rounded-xl p-0.5" : "gap-1 rounded-2xl p-1",
126
+ stretch ? "self-stretch" : "self-start",
127
+ className
128
+ )}
129
+ style={style}
130
+ {...viewProps}
131
+ >
132
+ <Animated.View
133
+ pointerEvents="none"
134
+ style={[
135
+ {
136
+ position: "absolute",
137
+ top: indicatorInset,
138
+ bottom: indicatorInset,
139
+ borderRadius: indicatorRadius,
140
+ backgroundColor: theme.card,
141
+ shadowColor: theme.text,
142
+ shadowOpacity: isGhost ? 0 : colorScheme === "dark" ? 0.08 : 0.14,
143
+ shadowRadius: isCompact ? 4 : 6,
144
+ shadowOffset: { width: 0, height: isCompact ? 1 : 2 },
145
+ elevation: isCompact ? 2 : 3
146
+ },
147
+ indicatorStyle
148
+ ]}
149
+ />
150
+ {tabs.map(({ key, label, icon, disabled: tabDisabled = false }) => {
151
+ const isDisabled = disabled || tabDisabled;
152
+
153
+ return (
154
+ <Pressable
155
+ key={String(key)}
156
+ onPress={() => handleTabPress(key)}
157
+ disabled={isDisabled}
158
+ onLayout={(e) => handleLayout(String(key), e.nativeEvent.layout)}
159
+ style={{
160
+ ...(stretch
161
+ ? { flex: 1, minWidth: 0 }
162
+ : { flexShrink: 1, minWidth: 0 }),
163
+ alignItems: "center",
164
+ justifyContent: "center",
165
+ paddingHorizontal: pressPadH,
166
+ paddingVertical: pressPadV,
167
+ borderRadius: pressRadius,
168
+ opacity: isDisabled ? 0.5 : 1
169
+ }}
170
+ >
171
+ <View
172
+ style={{
173
+ flexDirection: "row",
174
+ alignItems: "center",
175
+ gap: innerGap,
176
+ flexShrink: 1,
177
+ overflow: "hidden"
178
+ }}
179
+ >
180
+ {icon}
181
+ {label && (
182
+ <MText
183
+ size={isGhost ? "lg" : "xl"}
184
+ numberOfLines={1}
185
+ style={{ flexShrink: 1 }}
186
+ >
187
+ {label}
188
+ </MText>
189
+ )}
190
+ </View>
191
+ </Pressable>
192
+ );
193
+ })}
194
+ </View>
195
+ );
196
+ };
197
+
198
+ MTabs.displayName = "MTabs";