@mrmeg/expo-ui 0.6.1 → 0.7.1

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 (55) hide show
  1. package/LLM_USAGE.md +9 -6
  2. package/README.md +11 -7
  3. package/dist/components/Accordion.js +21 -16
  4. package/dist/components/AnimatedView.d.ts +1 -1
  5. package/dist/components/AnimatedView.js +2 -2
  6. package/dist/components/Badge.d.ts +3 -2
  7. package/dist/components/Badge.js +4 -3
  8. package/dist/components/BottomSheet.js +31 -29
  9. package/dist/components/BottomSheetKeyboard.d.ts +7 -0
  10. package/dist/components/BottomSheetKeyboard.js +35 -0
  11. package/dist/components/Button.d.ts +55 -13
  12. package/dist/components/Button.js +72 -28
  13. package/dist/components/Card.js +8 -10
  14. package/dist/components/Checkbox.js +22 -25
  15. package/dist/components/Collapsible.js +3 -7
  16. package/dist/components/Dialog.js +1 -1
  17. package/dist/components/DismissKeyboard.js +3 -3
  18. package/dist/components/Drawer.js +21 -10
  19. package/dist/components/DropdownMenu.d.ts +3 -2
  20. package/dist/components/DropdownMenu.js +29 -29
  21. package/dist/components/EmptyState.js +1 -1
  22. package/dist/components/InputOTP.js +16 -40
  23. package/dist/components/Notification.js +106 -27
  24. package/dist/components/Popover.js +1 -1
  25. package/dist/components/Progress.d.ts +2 -2
  26. package/dist/components/Progress.js +36 -34
  27. package/dist/components/RadioGroup.js +22 -20
  28. package/dist/components/Select.js +30 -20
  29. package/dist/components/Skeleton.js +6 -6
  30. package/dist/components/Slider.js +90 -97
  31. package/dist/components/StyledText.context.d.ts +6 -0
  32. package/dist/components/StyledText.context.js +5 -0
  33. package/dist/components/StyledText.d.ts +7 -58
  34. package/dist/components/StyledText.js +8 -28
  35. package/dist/components/Switch.js +30 -26
  36. package/dist/components/Tabs.d.ts +23 -3
  37. package/dist/components/Tabs.js +39 -17
  38. package/dist/components/TextInput.d.ts +6 -2
  39. package/dist/components/TextInput.js +6 -7
  40. package/dist/components/Toggle.js +12 -7
  41. package/dist/components/ToggleGroup.js +17 -11
  42. package/dist/components/Tooltip.js +1 -1
  43. package/dist/hooks/useDimensions.js +25 -26
  44. package/dist/hooks/useReduceMotion.d.ts +5 -1
  45. package/dist/hooks/useReduceMotion.js +46 -41
  46. package/dist/hooks/useResources.js +6 -1
  47. package/dist/hooks/useScalePress.d.ts +6 -5
  48. package/dist/hooks/useScalePress.js +25 -21
  49. package/dist/hooks/useStaggeredEntrance.d.ts +9 -8
  50. package/dist/hooks/useStaggeredEntrance.js +48 -21
  51. package/dist/state/globalUIStore.d.ts +23 -16
  52. package/dist/state/themeColorScope.js +3 -3
  53. package/llms-full.md +5 -6
  54. package/llms.txt +2 -2
  55. package/package.json +8 -6
@@ -2,17 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { palette } from "../constants/colors.js";
3
3
  import { useTheme } from "../hooks/useTheme.js";
4
4
  import { hapticLight } from "../lib/haptics.js";
5
- import { useCallback, useEffect, useRef } from "react";
6
- import { Platform, StyleSheet, View } from "react-native";
7
- import { Gesture, GestureDetector } from "react-native-gesture-handler";
8
- import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { Animated, PanResponder, Platform, StyleSheet, View } from "react-native";
9
7
  import { StyledText } from "./StyledText.js";
10
8
  const SIZES = {
11
9
  sm: { track: 4, thumb: 16 },
12
10
  md: { track: 6, thumb: 20 },
13
11
  };
14
12
  function clampAndSnap(raw, min, max, step) {
15
- "worklet";
16
13
  const clamped = Math.min(Math.max(raw, min), max);
17
14
  const stepped = Math.round((clamped - min) / step) * step + min;
18
15
  // Avoid floating-point drift
@@ -37,76 +34,70 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
37
34
  ? withAlpha(palette.white, 0.32)
38
35
  : theme.colors.mutedForeground
39
36
  : theme.colors.accent;
40
- // Track layout width captured via onLayout
41
- const trackWidth = useSharedValue(0);
42
- // Thumb position in pixels along the track
43
- const thumbX = useSharedValue(0);
44
- // Last snapped value (worklet-side) to detect step changes for haptics
45
- const lastSnappedValue = useSharedValue(value);
46
- // Keep a ref to onValueChange so the worklet always calls the latest version
47
- const onValueChangeRef = useRef(onValueChange);
48
- onValueChangeRef.current = onValueChange;
49
- const jsOnValueChange = useCallback((v) => {
50
- onValueChangeRef.current?.(v);
51
- }, []);
52
- const jsHaptic = useCallback(() => {
53
- hapticLight();
54
- }, []);
55
- // Sync external value prop changes after render. Reanimated warns when shared
56
- // values are read or written while React is rendering.
37
+ const [trackWidth, setTrackWidth] = useState(0);
38
+ const trackWidthRef = useRef(0);
39
+ const thumbX = useRef(new Animated.Value(0)).current;
40
+ const lastSnappedValue = useRef(value);
41
+ const updateFromPosition = useCallback((rawX) => {
42
+ const width = trackWidthRef.current;
43
+ const x = Math.min(Math.max(rawX, 0), width);
44
+ thumbX.stopAnimation();
45
+ thumbX.setValue(x);
46
+ const ratio = width > 0 ? x / width : 0;
47
+ const raw = min + ratio * (max - min);
48
+ const snapped = clampAndSnap(raw, min, max, step);
49
+ if (snapped !== lastSnappedValue.current) {
50
+ lastSnappedValue.current = snapped;
51
+ hapticLight();
52
+ }
53
+ onValueChange?.(snapped);
54
+ }, [max, min, onValueChange, step, thumbX]);
55
+ const handleGesture = useCallback((event) => {
56
+ updateFromPosition(event.nativeEvent.locationX);
57
+ }, [updateFromPosition]);
58
+ const panResponder = useMemo(() => PanResponder.create({
59
+ onStartShouldSetPanResponder: () => !disabled,
60
+ onMoveShouldSetPanResponder: () => !disabled,
61
+ onPanResponderGrant: handleGesture,
62
+ onPanResponderMove: handleGesture,
63
+ }), [disabled, handleGesture]);
57
64
  useEffect(() => {
58
65
  const ratio = getValueRatio(value, min, max);
59
- const width = trackWidth.value;
66
+ const width = trackWidthRef.current;
60
67
  if (width > 0) {
61
- thumbX.value = withTiming(ratio * width, { duration: 80 });
68
+ Animated.timing(thumbX, {
69
+ toValue: ratio * width,
70
+ duration: 80,
71
+ useNativeDriver: true,
72
+ }).start();
62
73
  }
63
- lastSnappedValue.value = value;
64
- }, [lastSnappedValue, max, min, thumbX, trackWidth, value]);
74
+ lastSnappedValue.current = value;
75
+ }, [max, min, thumbX, value]);
65
76
  const onTrackLayout = useCallback((e) => {
66
77
  const w = e.nativeEvent.layout.width;
67
- trackWidth.value = w;
78
+ trackWidthRef.current = w;
79
+ setTrackWidth(w);
68
80
  // Set initial thumb position without animation
69
81
  const ratio = getValueRatio(value, min, max);
70
- thumbX.value = ratio * w;
71
- }, [max, min, thumbX, trackWidth, value]);
72
- const panGesture = Gesture.Pan()
73
- .enabled(!disabled)
74
- .onBegin((e) => {
75
- "worklet";
76
- // Jump to touch position
77
- const x = Math.min(Math.max(e.x, 0), trackWidth.value);
78
- thumbX.value = x;
79
- const ratio = trackWidth.value > 0 ? x / trackWidth.value : 0;
80
- const raw = min + ratio * (max - min);
81
- const snapped = clampAndSnap(raw, min, max, step);
82
- if (snapped !== lastSnappedValue.value) {
83
- lastSnappedValue.value = snapped;
84
- runOnJS(jsHaptic)();
85
- }
86
- runOnJS(jsOnValueChange)(snapped);
87
- })
88
- .onUpdate((e) => {
89
- "worklet";
90
- const x = Math.min(Math.max(e.x, 0), trackWidth.value);
91
- thumbX.value = x;
92
- const ratio = trackWidth.value > 0 ? x / trackWidth.value : 0;
93
- const raw = min + ratio * (max - min);
94
- const snapped = clampAndSnap(raw, min, max, step);
95
- if (snapped !== lastSnappedValue.value) {
96
- lastSnappedValue.value = snapped;
97
- runOnJS(jsHaptic)();
98
- }
99
- runOnJS(jsOnValueChange)(snapped);
82
+ thumbX.stopAnimation();
83
+ thumbX.setValue(ratio * w);
84
+ }, [max, min, thumbX, value]);
85
+ const safeTrackWidth = Math.max(trackWidth, 1);
86
+ const fillScale = thumbX.interpolate({
87
+ inputRange: [0, safeTrackWidth],
88
+ outputRange: [0, 1],
89
+ extrapolate: "clamp",
90
+ });
91
+ const thumbTranslateX = thumbX.interpolate({
92
+ inputRange: [0, safeTrackWidth],
93
+ outputRange: [-dims.thumb / 2, safeTrackWidth - dims.thumb / 2],
94
+ extrapolate: "clamp",
95
+ });
96
+ const labelTranslateX = thumbX.interpolate({
97
+ inputRange: [0, safeTrackWidth],
98
+ outputRange: [-14, safeTrackWidth - 14],
99
+ extrapolate: "clamp",
100
100
  });
101
- const fillStyle = useAnimatedStyle(() => ({
102
- width: thumbX.value,
103
- }));
104
- const thumbAnimatedStyle = useAnimatedStyle(() => ({
105
- transform: [{ translateX: thumbX.value - dims.thumb / 2 }],
106
- }));
107
- const valueLabelStyle = useAnimatedStyle(() => ({
108
- transform: [{ translateX: thumbX.value - 14 }],
109
- }));
110
101
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
111
102
  // Accessibility action handler
112
103
  const handleAccessibilityAction = useCallback((event) => {
@@ -132,42 +123,44 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
132
123
  width: 28,
133
124
  alignItems: "center",
134
125
  },
135
- valueLabelStyle,
126
+ { transform: [{ translateX: labelTranslateX }] },
136
127
  { pointerEvents: "none" },
137
128
  ], children: _jsx(StyledText, { selectable: false, style: {
138
- fontSize: 11,
129
+ fontSize: 12,
139
130
  color: theme.colors.textDim,
140
131
  userSelect: "none",
141
- }, children: value }) })), _jsx(GestureDetector, { gesture: panGesture, children: _jsxs(View, { style: {
142
- height: dims.thumb,
143
- justifyContent: "center",
144
- ...(Platform.OS === "web" && { cursor: disabled ? "default" : "pointer" }),
145
- }, onLayout: onTrackLayout, children: [_jsx(View, { style: {
146
- height: dims.track,
147
- borderRadius: dims.track / 2,
148
- backgroundColor: inactiveTrackColor,
149
- overflow: "hidden",
150
- }, children: _jsx(Animated.View, { style: [
151
- {
152
- height: dims.track,
153
- borderRadius: dims.track / 2,
154
- backgroundColor: activeTrackColor,
155
- },
156
- fillStyle,
157
- ] }) }), _jsx(Animated.View, { style: [
132
+ }, children: value }) })), _jsxs(View, { style: {
133
+ height: dims.thumb,
134
+ justifyContent: "center",
135
+ ...(Platform.OS === "web" && { cursor: disabled ? "default" : "pointer" }),
136
+ }, onLayout: onTrackLayout, ...panResponder.panHandlers, children: [_jsx(View, { style: {
137
+ height: dims.track,
138
+ borderRadius: dims.track / 2,
139
+ backgroundColor: inactiveTrackColor,
140
+ overflow: "hidden",
141
+ }, children: _jsx(Animated.View, { style: [
158
142
  {
159
- position: "absolute",
160
- top: 0,
161
- left: 0,
162
- width: dims.thumb,
163
- height: dims.thumb,
164
- borderRadius: dims.thumb / 2,
165
- backgroundColor: thumbBackgroundColor,
166
- borderWidth: 1,
167
- borderColor: thumbBorderColor,
168
- ...getShadowStyle("subtle"),
143
+ width: "100%",
144
+ height: dims.track,
145
+ borderRadius: dims.track / 2,
146
+ backgroundColor: activeTrackColor,
147
+ transformOrigin: "left",
169
148
  },
170
- thumbAnimatedStyle,
171
- ] })] }) })] }));
149
+ { transform: [{ scaleX: fillScale }] },
150
+ ] }) }), _jsx(Animated.View, { style: [
151
+ {
152
+ position: "absolute",
153
+ top: 0,
154
+ left: 0,
155
+ width: dims.thumb,
156
+ height: dims.thumb,
157
+ borderRadius: dims.thumb / 2,
158
+ backgroundColor: thumbBackgroundColor,
159
+ borderWidth: 1,
160
+ borderColor: thumbBorderColor,
161
+ ...getShadowStyle("subtle"),
162
+ },
163
+ { transform: [{ translateX: thumbTranslateX }] },
164
+ ] })] })] }));
172
165
  }
173
166
  export { Slider };
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import type { StyleProp, TextStyle } from "react-native";
3
+ export declare const TextClassContext: React.Context<string | undefined>;
4
+ export declare const TextColorContext: React.Context<string | undefined>;
5
+ export declare const TextStyleContext: React.Context<StyleProp<TextStyle>>;
6
+ export declare const TextSelectabilityContext: React.Context<boolean | undefined>;
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ export const TextClassContext = React.createContext(undefined);
3
+ export const TextColorContext = React.createContext(undefined);
4
+ export const TextStyleContext = React.createContext(undefined);
5
+ export const TextSelectabilityContext = React.createContext(undefined);
@@ -1,25 +1,5 @@
1
- import React from "react";
2
- import { Text as RNText, TextProps as RNTextProps, StyleProp, TextStyle } from "react-native";
3
- /**
4
- * TextClassContext provides className context for nested text components
5
- * Used by @rn-primitives to apply consistent styling through the component tree
6
- */
7
- export declare const TextClassContext: React.Context<string | undefined>;
8
- /**
9
- * TextColorContext provides color context for nested text components
10
- * Allows parent components (like Button) to override text color for all children
11
- */
12
- export declare const TextColorContext: React.Context<string | undefined>;
13
- /**
14
- * TextStyleContext allows controls such as Button to pass sizing typography to
15
- * nested StyledText children without forcing consumers to use the `text` prop.
16
- */
17
- export declare const TextStyleContext: React.Context<StyleProp<TextStyle>>;
18
- /**
19
- * Allows interactive controls to disable text selection for nested StyledText
20
- * without changing the package-wide default for readable content.
21
- */
22
- export declare const TextSelectabilityContext: React.Context<boolean | undefined>;
1
+ import { type Ref } from "react";
2
+ import { Text as RNText, TextProps as RNTextProps } from "react-native";
23
3
  /**
24
4
  * Font size variants following the DM Sans / DM Serif Display scale
25
5
  */
@@ -71,6 +51,10 @@ export type TextProps = RNTextProps & {
71
51
  * as well as explicitly setting locale or translation fallbacks.
72
52
  */
73
53
  txOptions?: object;
54
+ /**
55
+ * Forwarded ref to the underlying RNText element.
56
+ */
57
+ ref?: Ref<RNText>;
74
58
  };
75
59
  /**
76
60
  * Base Text component with theme and variant support
@@ -85,42 +69,7 @@ export type TextProps = RNTextProps & {
85
69
  * chrome such as button labels, tabs, badges, and field labels
86
70
  * - numberOfLines and ellipsizeMode support from RN TextProps
87
71
  */
88
- export declare const StyledText: React.ForwardRefExoticComponent<RNTextProps & {
89
- /**
90
- * Font variant - serif (DM Serif Display) or sans-serif (DM Sans)
91
- */
92
- variant?: "serif" | "sansSerif";
93
- /**
94
- * Font weight - light, regular, medium, semibold, bold
95
- */
96
- fontWeight?: FontWeight;
97
- /**
98
- * Font size variant
99
- */
100
- size?: FontSize;
101
- /**
102
- * Semantic variant - automatically sets size and weight
103
- * Overrides individual size/fontWeight if provided
104
- */
105
- semantic?: SemanticVariant;
106
- /**
107
- * Text alignment
108
- */
109
- align?: TextAlign;
110
- /**
111
- * Text which is looked up via i18n.
112
- */
113
- tx?: string;
114
- /**
115
- * The text to display directly, or as fallback text when `tx` is provided.
116
- */
117
- text?: string;
118
- /**
119
- * Optional options to pass to i18n. Useful for interpolation
120
- * as well as explicitly setting locale or translation fallbacks.
121
- */
122
- txOptions?: object;
123
- } & React.RefAttributes<RNText>>;
72
+ export declare function StyledText(props: TextProps): import("react/jsx-runtime").JSX.Element;
124
73
  /**
125
74
  * Serif Text Component
126
75
  * Uses serif font family (DM Serif Display)
@@ -1,29 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React, { forwardRef } from "react";
2
+ import { use } from "react";
3
3
  import { Text as RNText, StyleSheet } from "react-native";
4
4
  import { useTheme } from "../hooks/useTheme.js";
5
5
  import { fontFamilies } from "../constants/fonts.js";
6
6
  import { translateText } from "../lib/i18n.js";
7
- /**
8
- * TextClassContext provides className context for nested text components
9
- * Used by @rn-primitives to apply consistent styling through the component tree
10
- */
11
- export const TextClassContext = React.createContext(undefined);
12
- /**
13
- * TextColorContext provides color context for nested text components
14
- * Allows parent components (like Button) to override text color for all children
15
- */
16
- export const TextColorContext = React.createContext(undefined);
17
- /**
18
- * TextStyleContext allows controls such as Button to pass sizing typography to
19
- * nested StyledText children without forcing consumers to use the `text` prop.
20
- */
21
- export const TextStyleContext = React.createContext(undefined);
22
- /**
23
- * Allows interactive controls to disable text selection for nested StyledText
24
- * without changing the package-wide default for readable content.
25
- */
26
- export const TextSelectabilityContext = React.createContext(undefined);
7
+ import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
27
8
  const FONT_SIZES = {
28
9
  xs: 11,
29
10
  sm: 12,
@@ -83,13 +64,13 @@ const getFontFamilyWeight = (weight) => {
83
64
  * chrome such as button labels, tabs, badges, and field labels
84
65
  * - numberOfLines and ellipsizeMode support from RN TextProps
85
66
  */
86
- export const StyledText = forwardRef((props, ref) => {
87
- const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, selectable, children, ...otherProps } = props;
67
+ export function StyledText(props) {
68
+ const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, selectable, children, ref, ...otherProps } = props;
88
69
  const { theme } = useTheme();
89
70
  // Check if there's a color override from parent context (e.g., Button)
90
- const contextColor = React.useContext(TextColorContext);
91
- const contextTextStyle = React.useContext(TextStyleContext);
92
- const contextSelectable = React.useContext(TextSelectabilityContext);
71
+ const contextColor = use(TextColorContext);
72
+ const contextTextStyle = use(TextStyleContext);
73
+ const contextSelectable = use(TextSelectabilityContext);
93
74
  const resolvedSelectable = selectable ?? contextSelectable ?? true;
94
75
  // Use context color if provided, otherwise use theme default
95
76
  const color = contextColor ?? theme.colors.text;
@@ -134,8 +115,7 @@ export const StyledText = forwardRef((props, ref) => {
134
115
  resolvedContextTextStyle,
135
116
  style,
136
117
  ], selectable: resolvedSelectable, ...otherProps, children: content }));
137
- });
138
- StyledText.displayName = "StyledText";
118
+ }
139
119
  /**
140
120
  * Serif Text Component
141
121
  * Uses serif font family (DM Serif Display)
@@ -6,9 +6,9 @@ import { useTheme } from "../hooks/useTheme.js";
6
6
  import { hapticLight } from "../lib/haptics.js";
7
7
  import * as SwitchPrimitives from "@rn-primitives/switch";
8
8
  import { useCallback, useEffect, useRef } from "react";
9
- import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
10
- import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolate, useReducedMotion, } from "react-native-reanimated";
9
+ import { ActivityIndicator, Platform, StyleSheet, View, Animated } from "react-native";
11
10
  import { StyledText } from "./StyledText.js";
11
+ import { useReducedMotion } from "../hooks/useReduceMotion.js";
12
12
  const DEFAULT_HIT_SLOP = 8;
13
13
  function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, ...props }) {
14
14
  const { theme, getContrastingColor, getShadowStyle, withAlpha } = useTheme();
@@ -23,27 +23,31 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
23
23
  useEffect(() => {
24
24
  hasMounted.current = true;
25
25
  }, []);
26
- // Single shared value drives everything: 0 = off, 1 = on
27
- const progress = useSharedValue(props.checked ? 1 : 0);
26
+ // Single animated value drives everything: 0 = off, 1 = on
27
+ const progress = useRef(new Animated.Value(props.checked ? 1 : 0)).current;
28
28
  useEffect(() => {
29
29
  const target = props.checked ? 1 : 0;
30
- if (reduceMotion) {
31
- progress.value = withTiming(target, { duration: 0 });
32
- }
33
- else {
34
- progress.value = withTiming(target, { duration: 120 });
35
- }
36
- }, [props.checked, reduceMotion]);
30
+ Animated.timing(progress, {
31
+ toValue: target,
32
+ duration: reduceMotion ? 0 : 120,
33
+ useNativeDriver: true,
34
+ }).start();
35
+ }, [props.checked, reduceMotion, progress]);
37
36
  // Thumb slides from left to right with equal inset on every side.
38
37
  const thumbInset = Math.max(2, (size.height - thumbSize) / 2);
39
38
  const thumbTravel = Math.max(0, size.width - thumbSize - thumbInset * 2);
40
39
  const labelGap = spacing.xs;
41
40
  const labelHorizontalInset = spacing.xs;
42
- const thumbAnimatedStyle = useAnimatedStyle(() => ({
41
+ const thumbAnimatedStyle = {
43
42
  transform: [
44
- { translateX: interpolate(progress.value, [0, 1], [0, thumbTravel]) },
43
+ {
44
+ translateX: progress.interpolate({
45
+ inputRange: [0, 1],
46
+ outputRange: [0, thumbTravel],
47
+ }),
48
+ },
45
49
  ],
46
- }));
50
+ };
47
51
  const isIOS = variant === "ios";
48
52
  const checkedColor = isIOS ? "#34C759" : theme.colors.accent;
49
53
  const uncheckedColor = theme.dark ? withAlpha(palette.white, 0.12) : theme.colors.input;
@@ -84,15 +88,10 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
84
88
  borderColor: trackBorderColor,
85
89
  pointerEvents: "none",
86
90
  } }), labelOn && !isIOS && (_jsx(View, { style: {
87
- position: "absolute",
88
- top: 0,
89
- bottom: 0,
91
+ ...styles.label,
90
92
  left: labelHorizontalInset,
91
93
  right: thumbInset + thumbSize + labelGap,
92
- justifyContent: "center",
93
- alignItems: "center",
94
94
  opacity: props.checked ? 1 : 0,
95
- pointerEvents: "none",
96
95
  }, children: _jsx(StyledText, { selectable: false, style: {
97
96
  fontFamily: fontFamilies.sansSerif.bold,
98
97
  fontSize: labelFontSize,
@@ -113,15 +112,10 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
113
112
  },
114
113
  thumbAnimatedStyle,
115
114
  ], children: loading && (_jsx(ActivityIndicator, { size: "small", color: thumbIndicatorColor })) }) }), labelOff && !isIOS && (_jsx(View, { style: {
116
- position: "absolute",
117
- top: 0,
118
- bottom: 0,
115
+ ...styles.label,
119
116
  left: thumbInset + thumbSize + labelGap,
120
117
  right: labelHorizontalInset,
121
- justifyContent: "center",
122
- alignItems: "center",
123
118
  opacity: props.checked ? 0 : 1,
124
- pointerEvents: "none",
125
119
  }, children: _jsx(StyledText, { selectable: false, style: {
126
120
  fontFamily: fontFamilies.sansSerif.bold,
127
121
  fontSize: labelFontSize,
@@ -129,4 +123,14 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
129
123
  userSelect: "none",
130
124
  }, children: labelOff }) }))] }));
131
125
  }
126
+ const styles = StyleSheet.create({
127
+ label: {
128
+ position: "absolute",
129
+ top: 0,
130
+ bottom: 0,
131
+ justifyContent: "center",
132
+ alignItems: "center",
133
+ pointerEvents: "none",
134
+ },
135
+ });
132
136
  export { Switch };
@@ -1,5 +1,7 @@
1
+ import * as React from "react";
1
2
  import { type StyleProp, type ViewStyle } from "react-native";
2
3
  import * as TabsPrimitive from "@rn-primitives/tabs";
4
+ import { StyledText } from "./StyledText";
3
5
  import { type IconName } from "./Icon";
4
6
  type TabsVariant = "underline" | "pill";
5
7
  type TabsSize = "sm" | "md";
@@ -17,15 +19,33 @@ export interface TabsTriggerProps extends TabsPrimitive.TriggerProps {
17
19
  style?: StyleProp<ViewStyle>;
18
20
  }
19
21
  declare function TabsTriggerInner({ icon, style, children, value, ...props }: TabsTriggerProps): import("react/jsx-runtime").JSX.Element;
20
- declare const TabsTrigger: typeof TabsTriggerInner;
22
+ /**
23
+ * TabsTrigger.Text
24
+ * Label text for a TabsTrigger. Reads the tab size from context and applies the
25
+ * matching font size, so callers state their intent explicitly instead of
26
+ * relying on the trigger to inspect `typeof children`. Text color is inherited
27
+ * from the trigger via TextColorContext.
28
+ *
29
+ * ```tsx
30
+ * <Tabs.Trigger value="account">
31
+ * <Tabs.Trigger.Text>Account</Tabs.Trigger.Text>
32
+ * </Tabs.Trigger>
33
+ * ```
34
+ */
35
+ declare function TabsTriggerText({ style, ...props }: React.ComponentProps<typeof StyledText>): import("react/jsx-runtime").JSX.Element;
21
36
  export interface TabsContentProps extends TabsPrimitive.ContentProps {
22
37
  style?: StyleProp<ViewStyle>;
23
38
  }
24
39
  declare function TabsContent({ style, children, ...props }: TabsContentProps): import("react/jsx-runtime").JSX.Element;
40
+ declare const TabsTriggerCompound: typeof TabsTriggerInner & {
41
+ Text: typeof TabsTriggerText;
42
+ };
25
43
  declare const Tabs: typeof TabsRoot & {
26
44
  List: typeof TabsList;
27
- Trigger: typeof TabsTriggerInner;
45
+ Trigger: typeof TabsTriggerInner & {
46
+ Text: typeof TabsTriggerText;
47
+ };
28
48
  Content: typeof TabsContent;
29
49
  };
30
- export { Tabs, TabsList, TabsTrigger, TabsContent, };
50
+ export { Tabs, TabsList, TabsTriggerCompound as TabsTrigger, TabsTriggerText, TabsContent, };
31
51
  export type { TabsVariant, TabsSize, };
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
- import { Platform, StyleSheet, View } from "react-native";
3
+ import { Animated, Platform, StyleSheet, View } from "react-native";
4
4
  import * as TabsPrimitive from "@rn-primitives/tabs";
5
- import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
6
- import { StyledText, TextClassContext, TextColorContext } from "./StyledText.js";
5
+ import { StyledText } from "./StyledText.js";
6
+ import { TextClassContext, TextColorContext } from "./StyledText.context";
7
7
  import { Icon } from "./Icon.js";
8
8
  import { useTheme } from "../hooks/useTheme.js";
9
+ import { useReducedMotion } from "../hooks/useReduceMotion.js";
9
10
  import { spacing } from "../constants/spacing.js";
10
11
  // ============================================================================
11
12
  // Size configs
@@ -29,10 +30,11 @@ const TabsContext = React.createContext({
29
30
  size: "md",
30
31
  });
31
32
  function useTabsContext() {
32
- return React.useContext(TabsContext);
33
+ return React.use(TabsContext);
33
34
  }
34
35
  function TabsRoot({ variant = "underline", size = "md", children, ...props }) {
35
- return (_jsx(TabsContext.Provider, { value: { variant, size }, children: _jsx(TabsPrimitive.Root, { ...props, children: children }) }));
36
+ const contextValue = React.useMemo(() => ({ variant, size }), [variant, size]);
37
+ return (_jsx(TabsContext.Provider, { value: contextValue, children: _jsx(TabsPrimitive.Root, { ...props, children: children }) }));
36
38
  }
37
39
  function TabsList({ style, children, ...props }) {
38
40
  const { theme } = useTheme();
@@ -62,15 +64,14 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
62
64
  // Determine selected state by comparing trigger value with root value
63
65
  const rootContext = TabsPrimitive.useRootContext();
64
66
  const isSelected = rootContext.value === value;
65
- const activeOpacity = useSharedValue(isSelected ? 1 : 0);
67
+ const activeOpacity = React.useRef(new Animated.Value(isSelected ? 1 : 0)).current;
66
68
  React.useEffect(() => {
67
- activeOpacity.value = reduceMotion
68
- ? (isSelected ? 1 : 0)
69
- : withTiming(isSelected ? 1 : 0, { duration: 200 });
70
- }, [isSelected, reduceMotion]);
71
- const indicatorStyle = useAnimatedStyle(() => ({
72
- opacity: activeOpacity.value,
73
- }));
69
+ Animated.timing(activeOpacity, {
70
+ toValue: isSelected ? 1 : 0,
71
+ duration: reduceMotion ? 0 : 200,
72
+ useNativeDriver: true,
73
+ }).start();
74
+ }, [isSelected, reduceMotion, activeOpacity]);
74
75
  const textColor = isDisabled
75
76
  ? theme.colors.mutedForeground
76
77
  : isSelected
@@ -92,7 +93,7 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
92
93
  borderRadius: spacing.radiusSm,
93
94
  ...getShadowStyle("subtle"),
94
95
  } : {};
95
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(TabsPrimitive.Trigger, { value: value, style: StyleSheet.flatten([triggerBaseStyle, pillActiveStyle, style]), ...props, children: [_jsxs(View, { style: triggerContentStyles.container, children: [icon && (_jsx(Icon, { name: icon, size: sizeConfig.iconSize, color: textColor, decorative: true })), typeof children === "string" ? (_jsx(StyledText, { selectable: false, style: { fontSize: sizeConfig.fontSize }, children: children })) : children] }), variant === "underline" && (_jsx(Animated.View, { style: [
96
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(TabsPrimitive.Trigger, { value: value, style: StyleSheet.flatten([triggerBaseStyle, pillActiveStyle, style]), ...props, children: [_jsxs(View, { style: triggerContentStyles.container, children: [icon && (_jsx(Icon, { name: icon, size: sizeConfig.iconSize, color: textColor, decorative: true })), children] }), variant === "underline" && (_jsx(Animated.View, { style: [
96
97
  {
97
98
  position: "absolute",
98
99
  bottom: 0,
@@ -101,10 +102,28 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
101
102
  height: 2,
102
103
  backgroundColor: theme.colors.foreground,
103
104
  },
104
- indicatorStyle,
105
+ { opacity: activeOpacity },
105
106
  ] }))] }) }) }));
106
107
  }
107
108
  const TabsTrigger = TabsTriggerInner;
109
+ /**
110
+ * TabsTrigger.Text
111
+ * Label text for a TabsTrigger. Reads the tab size from context and applies the
112
+ * matching font size, so callers state their intent explicitly instead of
113
+ * relying on the trigger to inspect `typeof children`. Text color is inherited
114
+ * from the trigger via TextColorContext.
115
+ *
116
+ * ```tsx
117
+ * <Tabs.Trigger value="account">
118
+ * <Tabs.Trigger.Text>Account</Tabs.Trigger.Text>
119
+ * </Tabs.Trigger>
120
+ * ```
121
+ */
122
+ function TabsTriggerText({ style, ...props }) {
123
+ const { size } = useTabsContext();
124
+ const { fontSize } = SIZE_CONFIGS[size];
125
+ return _jsx(StyledText, { selectable: false, style: [{ fontSize }, style], ...props });
126
+ }
108
127
  function TabsContent({ style, children, ...props }) {
109
128
  return (_jsx(TabsPrimitive.Content, { style: StyleSheet.flatten([{ marginTop: spacing.md }, style]), ...props, children: children }));
110
129
  }
@@ -122,9 +141,12 @@ const triggerContentStyles = StyleSheet.create({
122
141
  // ============================================================================
123
142
  // Compound Export
124
143
  // ============================================================================
144
+ const TabsTriggerCompound = Object.assign(TabsTrigger, {
145
+ Text: TabsTriggerText,
146
+ });
125
147
  const Tabs = Object.assign(TabsRoot, {
126
148
  List: TabsList,
127
- Trigger: TabsTrigger,
149
+ Trigger: TabsTriggerCompound,
128
150
  Content: TabsContent,
129
151
  });
130
- export { Tabs, TabsList, TabsTrigger, TabsContent, };
152
+ export { Tabs, TabsList, TabsTriggerCompound as TabsTrigger, TabsTriggerText, TabsContent, };