@mrmeg/expo-ui 0.6.1 → 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 +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 +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
@@ -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
  }
@@ -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);