@mrmeg/expo-ui 0.6.0 → 0.7.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 (54) hide show
  1. package/LLM_USAGE.md +4 -5
  2. package/README.md +6 -6
  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 +16 -12
  20. package/dist/components/DropdownMenu.js +32 -30
  21. package/dist/components/EmptyState.js +1 -1
  22. package/dist/components/InputOTP.js +16 -40
  23. package/dist/components/Notification.js +50 -25
  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/themeColorScope.js +3 -3
  52. package/llms-full.md +4 -5
  53. package/llms.txt +2 -2
  54. package/package.json +1 -4
@@ -1,15 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useCallback, useContext, useEffect, useRef } from "react";
3
- import { StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
4
- import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, useReducedMotion, Easing, } from "react-native-reanimated";
2
+ import { useMemo, useCallback, use, useEffect, useEffectEvent, useRef } from "react";
3
+ import { Animated, Easing, StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
5
4
  import { SafeAreaInsetsContext } from "react-native-safe-area-context";
6
5
  import { fontFamilies } from "../constants/fonts.js";
7
6
  import { Icon } from "./Icon.js";
8
7
  import { useTheme } from "../hooks/useTheme.js";
8
+ import { useReducedMotion } from "../hooks/useReduceMotion.js";
9
9
  import { spacing } from "../constants/spacing.js";
10
10
  import { StyledText } from "./StyledText.js";
11
11
  import { translateText } from "../lib/i18n.js";
12
12
  import { globalUIStore } from "../state/globalUIStore.js";
13
+ const timingIn = { duration: 150, easing: Easing.out(Easing.quad), useNativeDriver: true };
14
+ const timingOut = { duration: 100, easing: Easing.in(Easing.quad), useNativeDriver: true };
13
15
  /**
14
16
  * Notification
15
17
  *
@@ -38,34 +40,46 @@ import { globalUIStore } from "../state/globalUIStore.js";
38
40
  export const Notification = () => {
39
41
  const { theme, getShadowStyle } = useTheme();
40
42
  const reduceMotion = useReducedMotion();
41
- const insets = useContext(SafeAreaInsetsContext);
43
+ const insets = use(SafeAreaInsetsContext);
42
44
  const { alert, hide } = globalUIStore();
43
45
  const styles = useMemo(() => createStyles(theme), [theme]);
44
46
  const position = alert?.position ?? "top";
45
47
  const isBottom = position === "bottom";
46
48
  // Just opacity + translateY — no scale (scale = bouncy feel)
47
- const opacity = useSharedValue(0);
48
- const translateY = useSharedValue(0);
49
+ const opacity = useRef(new Animated.Value(0)).current;
50
+ const translateY = useRef(new Animated.Value(0)).current;
49
51
  const wasVisibleRef = useRef(false);
50
52
  const timerRef = useRef(null);
51
53
  const hideNotification = useCallback(() => {
52
54
  hide();
53
55
  }, [hide]);
54
- const timingIn = { duration: 150, easing: Easing.out(Easing.quad) };
55
- const timingOut = { duration: 100, easing: Easing.in(Easing.quad) };
56
56
  const animateOut = useCallback(() => {
57
57
  if (reduceMotion) {
58
- opacity.value = withTiming(0, { duration: 0 });
58
+ opacity.setValue(0);
59
59
  hideNotification();
60
60
  return;
61
61
  }
62
62
  const slideTarget = isBottom ? 8 : -8;
63
- opacity.value = withTiming(0, timingOut);
64
- translateY.value = withTiming(slideTarget, timingOut, (finished) => {
63
+ Animated.parallel([
64
+ Animated.timing(opacity, {
65
+ toValue: 0,
66
+ ...timingOut,
67
+ }),
68
+ Animated.timing(translateY, {
69
+ toValue: slideTarget,
70
+ ...timingOut,
71
+ }),
72
+ ]).start(({ finished }) => {
65
73
  if (finished)
66
- runOnJS(hideNotification)();
74
+ hideNotification();
67
75
  });
68
76
  }, [reduceMotion, isBottom, opacity, translateY, hideNotification]);
77
+ // The auto-dismiss timer only needs the latest animateOut; wrapping it in an
78
+ // Effect Event keeps it out of the deps so the effect doesn't re-run (and
79
+ // restart the timer) every time animateOut's identity changes.
80
+ const onAutoDismiss = useEffectEvent(() => {
81
+ animateOut();
82
+ });
69
83
  useEffect(() => {
70
84
  const isNowVisible = alert?.show ?? false;
71
85
  const wasVisible = wasVisibleRef.current;
@@ -76,20 +90,28 @@ export const Notification = () => {
76
90
  }
77
91
  const slideFrom = isBottom ? 8 : -8;
78
92
  if (reduceMotion) {
79
- opacity.value = withTiming(1, { duration: 0 });
80
- translateY.value = withTiming(0, { duration: 0 });
93
+ opacity.setValue(1);
94
+ translateY.setValue(0);
81
95
  }
82
96
  else {
83
- opacity.value = 0;
84
- translateY.value = slideFrom;
85
- opacity.value = withTiming(1, timingIn);
86
- translateY.value = withTiming(0, timingIn);
97
+ opacity.setValue(0);
98
+ translateY.setValue(slideFrom);
99
+ Animated.parallel([
100
+ Animated.timing(opacity, {
101
+ toValue: 1,
102
+ ...timingIn,
103
+ }),
104
+ Animated.timing(translateY, {
105
+ toValue: 0,
106
+ ...timingIn,
107
+ }),
108
+ ]).start();
87
109
  }
88
110
  }
89
111
  wasVisibleRef.current = isNowVisible;
90
112
  if (isNowVisible && !wasVisible && alert?.duration) {
91
113
  timerRef.current = setTimeout(() => {
92
- animateOut();
114
+ onAutoDismiss();
93
115
  }, alert.duration);
94
116
  return () => {
95
117
  if (timerRef.current) {
@@ -98,11 +120,12 @@ export const Notification = () => {
98
120
  }
99
121
  };
100
122
  }
101
- }, [alert, reduceMotion, isBottom, opacity, translateY, animateOut]);
102
- const animatedContainerStyle = useAnimatedStyle(() => ({
103
- opacity: opacity.value,
104
- transform: [{ translateY: translateY.value }],
105
- }));
123
+ // onAutoDismiss is an Effect Event — intentionally omitted from deps.
124
+ }, [alert, reduceMotion, isBottom, opacity, translateY]);
125
+ const animatedContainerStyle = {
126
+ opacity,
127
+ transform: [{ translateY }],
128
+ };
106
129
  const topPosition = insets?.top ? insets.top : 20;
107
130
  const bottomPosition = insets?.bottom ? insets.bottom : 20;
108
131
  const getIconProps = () => {
@@ -179,7 +202,9 @@ const createStyles = (theme) => StyleSheet.create({
179
202
  position: "absolute",
180
203
  left: spacing.md,
181
204
  right: spacing.md,
182
- zIndex: 1000,
205
+ // Toast sits above the overlay layer (dialogs/drawers/dropdowns top out
206
+ // around 52); no need to escalate into the hundreds.
207
+ zIndex: 60,
183
208
  alignItems: "center",
184
209
  },
185
210
  alert: {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { AnimatedView } from "./AnimatedView.js";
3
- import { TextClassContext, TextColorContext } from "./StyledText.js";
3
+ import { TextClassContext, TextColorContext } from "./StyledText.context";
4
4
  import { useTheme } from "../hooks/useTheme.js";
5
5
  import { spacing } from "../constants/spacing.js";
6
6
  import * as PopoverPrimitive from "@rn-primitives/popover";
@@ -15,8 +15,8 @@ export interface ProgressProps {
15
15
  * Progress Component
16
16
  *
17
17
  * A linear progress bar supporting determinate and indeterminate modes.
18
- * Determinate mode animates the fill width via Reanimated.
19
- * Indeterminate mode pulses opacity via RN core Animated.loop.
18
+ * Determinate mode animates the fill with React Native Animated.
19
+ * Indeterminate mode pulses opacity via Animated.loop.
20
20
  *
21
21
  * @example
22
22
  * ```tsx
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect, useRef } from "react";
3
3
  import { Animated, StyleSheet, View } from "react-native";
4
- import Reanimated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
5
4
  import { useTheme } from "../hooks/useTheme.js";
6
- import { shouldUseNativeDriver } from "../lib/animations.js";
5
+ import { useReducedMotion } from "../hooks/useReduceMotion.js";
7
6
  // ============================================================================
8
7
  // Constants
9
8
  // ============================================================================
@@ -19,8 +18,8 @@ const SIZE_MAP = {
19
18
  * Progress Component
20
19
  *
21
20
  * A linear progress bar supporting determinate and indeterminate modes.
22
- * Determinate mode animates the fill width via Reanimated.
23
- * Indeterminate mode pulses opacity via RN core Animated.loop.
21
+ * Determinate mode animates the fill with React Native Animated.
22
+ * Indeterminate mode pulses opacity via Animated.loop.
24
23
  *
25
24
  * @example
26
25
  * ```tsx
@@ -52,28 +51,28 @@ export function Progress({ value, variant = "default", size = "md", style, }) {
52
51
  ]), children: isDeterminate ? (_jsx(DeterminateFill, { value: value, fillColor: fillColor, height: height, borderRadius: borderRadius, reduceMotion: reduceMotion })) : (_jsx(IndeterminateFill, { fillColor: fillColor, height: height, borderRadius: borderRadius, reduceMotion: reduceMotion })) }));
53
52
  }
54
53
  function DeterminateFill({ value, fillColor, height, borderRadius, reduceMotion, }) {
55
- const [containerWidth, setContainerWidth] = useState(0);
56
54
  const clamped = Math.min(100, Math.max(0, value));
57
- const animatedWidth = useSharedValue(0);
55
+ // Animate scaleX (GPU compositor) instead of width (JS-thread layout each
56
+ // frame). transformOrigin "left" grows the fill from the left edge, so a
57
+ // full-width bar scaled by clamped/100 needs no container measurement.
58
+ const scaleX = useRef(new Animated.Value(0)).current;
58
59
  useEffect(() => {
59
- if (containerWidth === 0)
60
- return;
61
- const target = (clamped / 100) * containerWidth;
62
- animatedWidth.value = withTiming(target, {
60
+ Animated.timing(scaleX, {
61
+ toValue: clamped / 100,
63
62
  duration: reduceMotion ? 0 : 300,
64
- });
65
- }, [clamped, containerWidth, reduceMotion]);
66
- const animatedStyle = useAnimatedStyle(() => ({
67
- width: animatedWidth.value,
68
- }));
69
- return (_jsx(View, { style: { flex: 1 }, onLayout: (e) => setContainerWidth(e.nativeEvent.layout.width), children: _jsx(Reanimated.View, { style: [
70
- {
71
- height,
72
- borderRadius,
73
- backgroundColor: fillColor,
74
- },
75
- animatedStyle,
76
- ] }) }));
63
+ useNativeDriver: true,
64
+ }).start();
65
+ }, [clamped, reduceMotion, scaleX]);
66
+ return (_jsx(Animated.View, { style: [
67
+ {
68
+ width: "100%",
69
+ height,
70
+ borderRadius,
71
+ backgroundColor: fillColor,
72
+ transformOrigin: "left",
73
+ },
74
+ { transform: [{ scaleX }] },
75
+ ] }));
77
76
  }
78
77
  function IndeterminateFill({ fillColor, height, borderRadius, reduceMotion, }) {
79
78
  const opacity = useRef(new Animated.Value(reduceMotion ? 0.7 : 0.4)).current;
@@ -82,26 +81,29 @@ function IndeterminateFill({ fillColor, height, borderRadius, reduceMotion, }) {
82
81
  opacity.setValue(0.7);
83
82
  return;
84
83
  }
84
+ opacity.setValue(0.4);
85
85
  const animation = Animated.loop(Animated.sequence([
86
86
  Animated.timing(opacity, {
87
- toValue: 1.0,
87
+ toValue: 1,
88
88
  duration: 800,
89
- useNativeDriver: shouldUseNativeDriver,
89
+ useNativeDriver: true,
90
90
  }),
91
91
  Animated.timing(opacity, {
92
92
  toValue: 0.4,
93
93
  duration: 800,
94
- useNativeDriver: shouldUseNativeDriver,
94
+ useNativeDriver: true,
95
95
  }),
96
96
  ]));
97
97
  animation.start();
98
98
  return () => animation.stop();
99
99
  }, [opacity, reduceMotion]);
100
- return (_jsx(Animated.View, { style: {
101
- width: "40%",
102
- height,
103
- borderRadius,
104
- backgroundColor: fillColor,
105
- opacity,
106
- } }));
100
+ return (_jsx(Animated.View, { style: [
101
+ {
102
+ width: "40%",
103
+ height,
104
+ borderRadius,
105
+ backgroundColor: fillColor,
106
+ },
107
+ { opacity },
108
+ ] }));
107
109
  }
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect } from "react";
3
- import { View, StyleSheet, Pressable, Platform } from "react-native";
4
- import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
2
+ import React, { createContext, use, useEffect, useRef } from "react";
3
+ import { View, StyleSheet, Pressable, Platform, Animated } from "react-native";
5
4
  import { StyledText } from "./StyledText.js";
6
5
  import { useTheme } from "../hooks/useTheme.js";
7
6
  import { spacing } from "../constants/spacing.js";
8
7
  import { hapticLight } from "../lib/haptics.js";
8
+ import { useReducedMotion } from "../hooks/useReduceMotion.js";
9
9
  import * as RadioGroupPrimitive from "@rn-primitives/radio-group";
10
10
  const DEFAULT_HIT_SLOP = 8;
11
11
  const SIZE_CONFIGS = {
@@ -14,8 +14,11 @@ const SIZE_CONFIGS = {
14
14
  lg: { outer: 24, inner: 12, borderWidth: 1 },
15
15
  };
16
16
  const RadioGroupContext = createContext(null);
17
+ function handleRadioPress() {
18
+ hapticLight();
19
+ }
17
20
  function useRadioGroupContext() {
18
- const context = useContext(RadioGroupContext);
21
+ const context = use(RadioGroupContext);
19
22
  if (context === null) {
20
23
  throw new Error("RadioGroup compound components cannot be rendered outside the RadioGroup component");
21
24
  }
@@ -38,7 +41,8 @@ function useRadioGroupContext() {
38
41
  */
39
42
  function RadioGroupRoot({ size = "md", error = false, value, onValueChange, style: styleOverride, children, ...props }) {
40
43
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
41
- return (_jsx(RadioGroupContext.Provider, { value: { size, error, value, onValueChange }, children: _jsx(RadioGroupPrimitive.Root, { ...props, value: value, onValueChange: onValueChange, style: {
44
+ const contextValue = React.useMemo(() => ({ size, error, value, onValueChange }), [size, error, value, onValueChange]);
45
+ return (_jsx(RadioGroupContext.Provider, { value: contextValue, children: _jsx(RadioGroupPrimitive.Root, { ...props, value: value, onValueChange: onValueChange, style: {
42
46
  flexDirection: "column",
43
47
  gap: spacing.listItemSpacing,
44
48
  ...(flattenedStyle || {}),
@@ -56,19 +60,14 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
56
60
  const sizeConfig = SIZE_CONFIGS[size];
57
61
  const isChecked = groupValue === itemValue;
58
62
  // Animated dot scale — follows Checkbox opacity pattern
59
- const dotScale = useSharedValue(isChecked ? 1 : 0);
63
+ const dotScale = useRef(new Animated.Value(isChecked ? 1 : 0)).current;
60
64
  useEffect(() => {
61
- dotScale.value = reduceMotion
62
- ? (isChecked ? 1 : 0)
63
- : withTiming(isChecked ? 1 : 0, { duration: 60 });
65
+ Animated.timing(dotScale, {
66
+ toValue: isChecked ? 1 : 0,
67
+ duration: reduceMotion ? 0 : 60,
68
+ useNativeDriver: true,
69
+ }).start();
64
70
  }, [isChecked, reduceMotion, dotScale]);
65
- const dotStyle = useAnimatedStyle(() => ({
66
- transform: [{ scale: dotScale.value }],
67
- }));
68
- // Wrap onPress to add haptic feedback
69
- const handlePress = () => {
70
- hapticLight();
71
- };
72
71
  // Border color follows Checkbox pattern
73
72
  const borderColor = error
74
73
  ? theme.colors.destructive
@@ -76,20 +75,19 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
76
75
  ? theme.colors.primary
77
76
  : getContrastingColor(theme.colors.background, theme.colors.text, theme.colors.textDim);
78
77
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
79
- const radioElement = (_jsx(RadioGroupPrimitive.Item, { ...props, value: itemValue, disabled: disabled, onPress: handlePress, style: {
78
+ const radioElement = (_jsx(RadioGroupPrimitive.Item, { ...props, value: itemValue, disabled: disabled, onPress: handleRadioPress, style: {
79
+ ...styles.radio,
80
80
  borderColor,
81
81
  backgroundColor: theme.colors.background,
82
82
  borderRadius: sizeConfig.outer / 2,
83
83
  borderWidth: sizeConfig.borderWidth,
84
84
  width: sizeConfig.outer,
85
85
  height: sizeConfig.outer,
86
- justifyContent: "center",
87
- alignItems: "center",
88
86
  opacity: disabled ? 0.5 : 1,
89
87
  ...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
90
88
  ...(flattenedStyle || {}),
91
89
  }, hitSlop: DEFAULT_HIT_SLOP, accessibilityLabel: label, children: _jsx(Animated.View, { style: [
92
- dotStyle,
90
+ { transform: [{ scale: dotScale }] },
93
91
  {
94
92
  width: sizeConfig.inner,
95
93
  height: sizeConfig.inner,
@@ -118,6 +116,10 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
118
116
  ], children: [label, required && (_jsx(StyledText, { selectable: false, style: [styles.required, { color: theme.colors.destructive }], children: " *" }))] }) })] }));
119
117
  }
120
118
  const styles = StyleSheet.create({
119
+ radio: {
120
+ justifyContent: "center",
121
+ alignItems: "center",
122
+ },
121
123
  container: {
122
124
  flexDirection: "row",
123
125
  alignItems: "center",
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import { Platform, StyleSheet, View } from "react-native";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { AnimatedView } from "./AnimatedView.js";
6
- import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.js";
6
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
7
7
  import { useTheme } from "../hooks/useTheme.js";
8
8
  import { spacing } from "../constants/spacing.js";
9
9
  import * as SelectPrimitive from "@rn-primitives/select";
@@ -37,14 +37,10 @@ function SelectTrigger({ size = "md", error = false, children, style: styleOverr
37
37
  const { theme } = useTheme();
38
38
  const sizeConfig = SIZE_CONFIGS[size];
39
39
  return (_jsx(SelectPrimitive.Trigger, { disabled: disabled, ...props, style: {
40
- flexDirection: "row",
41
- justifyContent: "space-between",
42
- alignItems: "center",
40
+ ...styles.trigger,
43
41
  height: sizeConfig.height,
44
42
  paddingHorizontal: sizeConfig.paddingHorizontal,
45
- borderWidth: 1,
46
43
  borderColor: error ? theme.colors.destructive : theme.colors.border,
47
- borderRadius: spacing.radiusMd,
48
44
  backgroundColor: theme.colors.background,
49
45
  ...(Platform.OS === "web" && {
50
46
  cursor: disabled ? "not-allowed" : "pointer",
@@ -98,19 +94,13 @@ function SelectContent({ side, align = "start", sideOffset = 4, portalHost, styl
98
94
  }
99
95
  function SelectItem({ children, style: styleOverride, ...props }) {
100
96
  const { theme } = useTheme();
101
- const shouldRenderDefaultText = children == null ||
102
- typeof children === "string" ||
103
- typeof children === "number";
97
+ // Render custom element/array children when provided; otherwise fall back to
98
+ // the default ItemText driven by the required `label` prop. Discriminating on
99
+ // "is this a renderable node?" keeps the API explicit without switching on the
100
+ // primitive type of children.
101
+ const hasCustomChildren = React.isValidElement(children) || Array.isArray(children);
104
102
  return (_jsx(TextClassContext.Provider, { value: "", children: _jsxs(SelectPrimitive.Item, { ...props, style: {
105
- position: "relative",
106
- flexDirection: "row",
107
- alignItems: "center",
108
- gap: spacing.sm,
109
- borderRadius: spacing.radiusSm,
110
- paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
111
- paddingLeft: spacing.xl,
112
- paddingRight: spacing.sm,
113
- backgroundColor: "transparent",
103
+ ...styles.item,
114
104
  ...(Platform.OS === "web" && {
115
105
  cursor: props.disabled ? "not-allowed" : "pointer",
116
106
  outlineStyle: "none",
@@ -127,11 +117,11 @@ function SelectItem({ children, style: styleOverride, ...props }) {
127
117
  width: 14,
128
118
  alignItems: "center",
129
119
  justifyContent: "center",
130
- }, children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.accent, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), _jsx(TextSelectabilityContext.Provider, { value: false, children: shouldRenderDefaultText ? (_jsx(SelectPrimitive.ItemText, { style: {
120
+ }, children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.accent, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), _jsx(TextSelectabilityContext.Provider, { value: false, children: hasCustomChildren ? (children) : (_jsx(SelectPrimitive.ItemText, { style: {
131
121
  color: theme.colors.popoverForeground,
132
122
  fontSize: 14,
133
123
  lineHeight: 20,
134
- } })) : typeof children === "function" ? null : (children) })] }) }));
124
+ } })) })] }) }));
135
125
  }
136
126
  function SelectGroup({ style: styleOverride, ...props }) {
137
127
  return (_jsx(SelectPrimitive.Group, { ...props, style: {
@@ -166,6 +156,26 @@ function SelectSeparator({ style: styleOverride, ...props }) {
166
156
  : {}),
167
157
  } }));
168
158
  }
159
+ const styles = StyleSheet.create({
160
+ trigger: {
161
+ flexDirection: "row",
162
+ justifyContent: "space-between",
163
+ alignItems: "center",
164
+ borderWidth: 1,
165
+ borderRadius: spacing.radiusMd,
166
+ },
167
+ item: {
168
+ position: "relative",
169
+ flexDirection: "row",
170
+ alignItems: "center",
171
+ gap: spacing.sm,
172
+ borderRadius: spacing.radiusSm,
173
+ paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
174
+ paddingLeft: spacing.xl,
175
+ paddingRight: spacing.sm,
176
+ backgroundColor: "transparent",
177
+ },
178
+ });
169
179
  /**
170
180
  * Select Component with Sub-components
171
181
  * Properly typed interface for dot notation access (e.g., Select.Trigger)
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
- import { View, Animated, StyleSheet } from "react-native";
4
- import { useReducedMotion } from "react-native-reanimated";
3
+ import { View, StyleSheet, Animated } from "react-native";
5
4
  import { useTheme } from "../hooks/useTheme.js";
6
- import { shouldUseNativeDriver } from "../lib/animations.js";
5
+ import { useReducedMotion } from "../hooks/useReduceMotion.js";
7
6
  import { spacing } from "../constants/spacing.js";
8
7
  /**
9
8
  * Skeleton Component
@@ -26,16 +25,17 @@ export function Skeleton({ width = "100%", height = 20, borderRadius = spacing.r
26
25
  opacity.setValue(0.6);
27
26
  return;
28
27
  }
28
+ opacity.setValue(0.3);
29
29
  const animation = Animated.loop(Animated.sequence([
30
30
  Animated.timing(opacity, {
31
31
  toValue: 1,
32
32
  duration: 800,
33
- useNativeDriver: shouldUseNativeDriver,
33
+ useNativeDriver: true,
34
34
  }),
35
35
  Animated.timing(opacity, {
36
36
  toValue: 0.3,
37
37
  duration: 800,
38
- useNativeDriver: shouldUseNativeDriver,
38
+ useNativeDriver: true,
39
39
  }),
40
40
  ]));
41
41
  animation.start();
@@ -48,8 +48,8 @@ export function Skeleton({ width = "100%", height = 20, borderRadius = spacing.r
48
48
  height: circle ? resolvedSize : height,
49
49
  borderRadius: circle ? (resolvedSize / 2) : borderRadius,
50
50
  backgroundColor: theme.colors.muted,
51
- opacity,
52
51
  },
52
+ { opacity },
53
53
  style,
54
54
  ] }));
55
55
  }