@webority-technologies/mobile 0.0.12 → 0.0.14

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 (47) hide show
  1. package/lib/commonjs/components/Badge/Badge.js +1 -1
  2. package/lib/commonjs/components/BottomNavigation/BottomNavigation.js +11 -3
  3. package/lib/commonjs/components/BottomSheet/BottomSheet.js +45 -6
  4. package/lib/commonjs/components/DatePicker/DatePicker.js +18 -12
  5. package/lib/commonjs/components/DateRangePicker/DateRangePicker.js +14 -9
  6. package/lib/commonjs/components/FloatingActionButton/FloatingActionButton.js +1 -1
  7. package/lib/commonjs/components/Input/Input.js +1 -1
  8. package/lib/commonjs/components/Modal/Modal.js +4 -4
  9. package/lib/commonjs/components/OTPInput/OTPInput.js +29 -9
  10. package/lib/commonjs/components/ProgressBar/ProgressBar.js +1 -1
  11. package/lib/commonjs/components/SegmentedControl/SegmentedControl.js +23 -28
  12. package/lib/commonjs/components/Skeleton/Skeleton.js +1 -1
  13. package/lib/commonjs/components/Slider/Slider.js +11 -11
  14. package/lib/commonjs/components/Stepper/Stepper.js +10 -4
  15. package/lib/commonjs/components/Tabs/Tabs.js +7 -5
  16. package/lib/commonjs/components/TimePicker/TimePicker.js +3 -3
  17. package/lib/commonjs/components/Toast/Toast.js +2 -2
  18. package/lib/commonjs/theme/animatedValue.js +20 -1
  19. package/lib/commonjs/theme/index.js +8 -1
  20. package/lib/module/components/Badge/Badge.js +2 -2
  21. package/lib/module/components/BottomNavigation/BottomNavigation.js +11 -3
  22. package/lib/module/components/BottomSheet/BottomSheet.js +47 -8
  23. package/lib/module/components/DatePicker/DatePicker.js +19 -13
  24. package/lib/module/components/DateRangePicker/DateRangePicker.js +15 -10
  25. package/lib/module/components/FloatingActionButton/FloatingActionButton.js +2 -2
  26. package/lib/module/components/Input/Input.js +2 -2
  27. package/lib/module/components/Modal/Modal.js +5 -5
  28. package/lib/module/components/OTPInput/OTPInput.js +30 -10
  29. package/lib/module/components/ProgressBar/ProgressBar.js +2 -2
  30. package/lib/module/components/SegmentedControl/SegmentedControl.js +24 -29
  31. package/lib/module/components/Skeleton/Skeleton.js +2 -2
  32. package/lib/module/components/Slider/Slider.js +12 -12
  33. package/lib/module/components/Stepper/Stepper.js +10 -4
  34. package/lib/module/components/Tabs/Tabs.js +7 -5
  35. package/lib/module/components/TimePicker/TimePicker.js +4 -4
  36. package/lib/module/components/Toast/Toast.js +3 -3
  37. package/lib/module/theme/animatedValue.js +18 -0
  38. package/lib/module/theme/index.js +1 -1
  39. package/lib/typescript/commonjs/components/BottomNavigation/BottomNavigation.d.ts +7 -0
  40. package/lib/typescript/commonjs/components/BottomSheet/BottomSheet.d.ts +10 -0
  41. package/lib/typescript/commonjs/theme/animatedValue.d.ts +11 -0
  42. package/lib/typescript/commonjs/theme/index.d.ts +1 -1
  43. package/lib/typescript/module/components/BottomNavigation/BottomNavigation.d.ts +7 -0
  44. package/lib/typescript/module/components/BottomSheet/BottomSheet.d.ts +10 -0
  45. package/lib/typescript/module/theme/animatedValue.d.ts +11 -0
  46. package/lib/typescript/module/theme/index.d.ts +1 -1
  47. package/package.json +1 -1
@@ -47,15 +47,17 @@ const Tabs = exports.Tabs = /*#__PURE__*/(0, _react.forwardRef)((props, ref) =>
47
47
  (0, _react.useEffect)(() => {
48
48
  if (!activeLayout) return;
49
49
  const spring = theme.motion.spring.snappy;
50
- _reactNative.Animated.parallel([_reactNative.Animated.spring(indicatorTranslateX, {
50
+ _reactNative.Animated.parallel([
51
+ // Both must use the JS driver: width can't run on native, and mixing
52
+ // drivers on the same view (transform native + width JS) trips RN's
53
+ // "node already moved to native" guard under the new architecture.
54
+ _reactNative.Animated.spring(indicatorTranslateX, {
51
55
  toValue: activeLayout.x,
52
56
  damping: spring.damping,
53
57
  stiffness: spring.stiffness,
54
58
  mass: spring.mass,
55
- useNativeDriver: true
56
- }),
57
- // width is a layout prop — must run on JS thread (useNativeDriver: false).
58
- _reactNative.Animated.spring(indicatorWidth, {
59
+ useNativeDriver: false
60
+ }), _reactNative.Animated.spring(indicatorWidth, {
59
61
  toValue: activeLayout.width,
60
62
  damping: spring.damping,
61
63
  stiffness: spring.stiffness,
@@ -74,7 +74,7 @@ const Wheel = ({
74
74
  if (lastIndexRef.current !== selectedIndex) {
75
75
  lastIndexRef.current = selectedIndex;
76
76
  const offset = selectedIndex * ITEM_HEIGHT;
77
- scrollY.setValue(offset);
77
+ (0, _index.setNativeValue)(scrollY, offset);
78
78
  // Defer to next frame so FlatList has measured.
79
79
  requestAnimationFrame(() => {
80
80
  listRef.current?.scrollToOffset({
@@ -281,8 +281,8 @@ const TimePicker = ({
281
281
  useNativeDriver: true
282
282
  })]).start();
283
283
  } else {
284
- opacity.setValue(0);
285
- translateY.setValue(40);
284
+ (0, _index.setNativeValue)(opacity, 0);
285
+ (0, _index.setNativeValue)(translateY, 40);
286
286
  }
287
287
  }, [visible, opacity, translateY, theme.motion]);
288
288
  const announce = (0, _react.useCallback)(msg => {
@@ -73,9 +73,9 @@ const Toast = ({
73
73
  const panResponder = (0, _react.useMemo)(() => _reactNative.PanResponder.create({
74
74
  onMoveShouldSetPanResponder: (_evt, gesture) => Math.abs(gesture.dx) > Math.abs(gesture.dy) && Math.abs(gesture.dx) > 6,
75
75
  onPanResponderMove: (_evt, gesture) => {
76
- translateX.setValue(gesture.dx);
76
+ (0, _index.setNativeValue)(translateX, gesture.dx);
77
77
  const fade = 1 - Math.min(Math.abs(gesture.dx) / 200, 1) * 0.7;
78
- opacity.setValue(fade);
78
+ (0, _index.setNativeValue)(opacity, fade);
79
79
  },
80
80
  onPanResponderRelease: (_evt, gesture) => {
81
81
  if (Math.abs(gesture.dx) > SWIPE_DISMISS_THRESHOLD || Math.abs(gesture.vx) > SWIPE_VELOCITY_THRESHOLD) {
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createAnimatedValue = void 0;
6
+ exports.setNativeValue = exports.createAnimatedValue = void 0;
7
7
  var _reactNative = require("react-native");
8
8
  /**
9
9
  * Create an `Animated.Value` that survives RN 0.85's dev-mode prop deepFreeze.
@@ -24,5 +24,24 @@ var _reactNative = require("react-native");
24
24
  * Use everywhere the library would otherwise call `new Animated.Value(...)`.
25
25
  */
26
26
  const createAnimatedValue = (initial, config) => Object.seal(new _reactNative.Animated.Value(initial, config));
27
+
28
+ /**
29
+ * Set an `Animated.Value` to a target without going through JS.
30
+ *
31
+ * Once a value has been driven by `useNativeDriver: true`, the underlying
32
+ * native node owns it — calling `value.setValue(x)` from JS throws
33
+ * "Attempting to run JS driven animation on animated node that has been moved to native".
34
+ * Use this helper for any value that is *also* used in a native-driven
35
+ * `Animated.timing/spring`. A zero-duration native timing routes the update
36
+ * through the same driver and stays valid across re-mounts and re-runs.
37
+ */
27
38
  exports.createAnimatedValue = createAnimatedValue;
39
+ const setNativeValue = (value, to) => {
40
+ _reactNative.Animated.timing(value, {
41
+ toValue: to,
42
+ duration: 0,
43
+ useNativeDriver: true
44
+ }).start();
45
+ };
46
+ exports.setNativeValue = setNativeValue;
28
47
  //# sourceMappingURL=animatedValue.js.map
@@ -52,7 +52,14 @@ Object.defineProperty(exports, "mergeTheme", {
52
52
  return _merge.mergeTheme;
53
53
  }
54
54
  });
55
- exports.subscribeTheme = exports.setTheme = exports.setColorMode = exports.resetTheme = void 0;
55
+ exports.setColorMode = exports.resetTheme = void 0;
56
+ Object.defineProperty(exports, "setNativeValue", {
57
+ enumerable: true,
58
+ get: function () {
59
+ return _animatedValue.setNativeValue;
60
+ }
61
+ });
62
+ exports.subscribeTheme = exports.setTheme = void 0;
56
63
  Object.defineProperty(exports, "useTheme", {
57
64
  enumerable: true,
58
65
  get: function () {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, useEffect, useMemo, useRef } from 'react';
4
4
  import { Animated, Easing, StyleSheet, Text, View } from 'react-native';
5
- import { fontFor, useTheme, createAnimatedValue } from "../../theme/index.js";
5
+ import { fontFor, useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { SkeletonContent } from "../Skeleton/index.js";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  const toneFor = (theme, tone) => {
@@ -126,7 +126,7 @@ const Badge = /*#__PURE__*/forwardRef((props, ref) => {
126
126
  const pulseScale = useRef(createAnimatedValue(1)).current;
127
127
  useEffect(() => {
128
128
  if (!pulse || !shouldRender) {
129
- pulseScale.setValue(1);
129
+ setNativeValue(pulseScale, 1);
130
130
  return;
131
131
  }
132
132
  const loop = Animated.loop(Animated.sequence([Animated.timing(pulseScale, {
@@ -48,6 +48,7 @@ const BottomNavigation = /*#__PURE__*/forwardRef((props, ref) => {
48
48
  haptic = 'selection',
49
49
  showLabels = true,
50
50
  variant = 'pill',
51
+ indicatorPosition = 'bottom',
51
52
  style,
52
53
  indicatorStyle,
53
54
  labelStyle,
@@ -193,7 +194,7 @@ const BottomNavigation = /*#__PURE__*/forwardRef((props, ref) => {
193
194
  }, tab.key);
194
195
  }), variant === 'underline' && tabWidth > 0 ? /*#__PURE__*/_jsx(Animated.View, {
195
196
  pointerEvents: "none",
196
- style: [styles.underline, {
197
+ style: [styles.underline, indicatorPosition === 'top' ? styles.underlineTop : styles.underlineBottom, {
197
198
  width: tabWidth,
198
199
  backgroundColor: theme.colors.primary,
199
200
  transform: [{
@@ -243,12 +244,19 @@ const buildStyles = theme => StyleSheet.create({
243
244
  },
244
245
  underline: {
245
246
  position: 'absolute',
246
- bottom: -6,
247
247
  left: 0,
248
- height: UNDERLINE_HEIGHT,
248
+ height: UNDERLINE_HEIGHT
249
+ },
250
+ underlineBottom: {
251
+ bottom: -6,
249
252
  borderTopLeftRadius: 2,
250
253
  borderTopRightRadius: 2
251
254
  },
255
+ underlineTop: {
256
+ top: -6,
257
+ borderBottomLeftRadius: 2,
258
+ borderBottomRightRadius: 2
259
+ },
252
260
  badgePill: {
253
261
  position: 'absolute',
254
262
  top: -4,
@@ -20,8 +20,8 @@
20
20
  */
21
21
 
22
22
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
23
- import { Dimensions, Keyboard, Platform, Pressable, StyleSheet, View } from 'react-native';
24
- import { Gesture, GestureDetector } from 'react-native-gesture-handler';
23
+ import { Dimensions, Keyboard, Modal, Platform, Pressable, StyleSheet, View } from 'react-native';
24
+ import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
25
25
  import Animated, { Extrapolation, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
26
26
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
27
27
  import { useTheme } from "../../theme/index.js";
@@ -44,6 +44,7 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
44
44
  enableBackdropPress = true,
45
45
  backdropOpacity = 0.5,
46
46
  keyboardBehavior = 'none',
47
+ mode = 'modal',
47
48
  handleIndicatorStyle,
48
49
  containerStyle,
49
50
  children,
@@ -79,6 +80,10 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
79
80
  const currentIndexShared = useSharedValue(-1);
80
81
  const [currentIndex, setCurrentIndex] = useState(-1);
81
82
  const isAnimatingRef = useRef(false);
83
+ // Drives the native <Modal>'s visible prop in modal mode. We mount the modal
84
+ // synchronously on open and unmount only after the close animation finishes
85
+ // so the slide-down stays visible.
86
+ const [modalVisible, setModalVisible] = useState(false);
82
87
 
83
88
  // Convert a snap-point index → translateY position. -1 = closed.
84
89
  const yForIndex = useCallback(idx => {
@@ -106,14 +111,26 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
106
111
  }, [onAnimate, translateY]);
107
112
  const markAnimationDone = useCallback(() => {
108
113
  isAnimatingRef.current = false;
109
- }, []);
114
+ // If we just finished a close animation, unmount the modal wrapper.
115
+ if (mode === 'modal' && currentIndexShared.value < 0) {
116
+ setModalVisible(false);
117
+ }
118
+ }, [mode, currentIndexShared]);
110
119
  const expand = useCallback(idx => {
111
120
  const target = typeof idx === 'number' ? clamp(idx, 0, resolvedSnapPoints.length - 1) : currentIndex >= 0 ? currentIndex : 0;
112
121
  const fromIndex = currentIndexShared.value;
113
122
  const to = yForIndex(target);
123
+ // Mount the Modal before kicking off the spring so the sheet has a
124
+ // host to animate into.
125
+ if (mode === 'modal') setModalVisible(true);
126
+ // Reset translateY to the closed position so the first open animates
127
+ // up from off-screen rather than snapping in place.
128
+ if (fromIndex < 0) {
129
+ translateY.value = closedY;
130
+ }
114
131
  setIndexJS(target);
115
132
  animateTo(to, fromIndex, target);
116
- }, [animateTo, currentIndex, currentIndexShared, resolvedSnapPoints.length, setIndexJS, yForIndex]);
133
+ }, [animateTo, closedY, currentIndex, currentIndexShared, mode, resolvedSnapPoints.length, setIndexJS, translateY, yForIndex]);
117
134
  const collapse = useCallback(() => {
118
135
  if (resolvedSnapPoints.length === 0) return;
119
136
  const fromIndex = currentIndexShared.value;
@@ -295,16 +312,19 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
295
312
  }, [enableBackdropPress, close]);
296
313
 
297
314
  // Don't render the heavy gesture tree at all when nothing's been opened yet.
298
- // We still mount once `currentIndex` 0 OR an animation has started.
299
- // For controlled `index`, we treat any value ≠ -1 as "opened".
315
+ // For inline mode we keep the legacy "mount on first open" behavior; modal
316
+ // mode is gated entirely by `modalVisible`.
300
317
  const everOpenedRef = useRef(false);
301
318
  if (isExpanded || controlledIndex !== undefined) {
302
319
  everOpenedRef.current = true;
303
320
  }
304
- if (!everOpenedRef.current) {
321
+ if (mode === 'inline' && !everOpenedRef.current) {
322
+ return null;
323
+ }
324
+ if (mode === 'modal' && !modalVisible) {
305
325
  return null;
306
326
  }
307
- return /*#__PURE__*/_jsxs(View, {
327
+ const sheetTree = /*#__PURE__*/_jsxs(View, {
308
328
  style: StyleSheet.absoluteFill,
309
329
  pointerEvents: "box-none",
310
330
  testID: testID,
@@ -351,6 +371,22 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
351
371
  })
352
372
  })]
353
373
  });
374
+ if (mode === 'modal') {
375
+ return /*#__PURE__*/_jsx(Modal, {
376
+ transparent: true,
377
+ visible: modalVisible,
378
+ onRequestClose: close,
379
+ statusBarTranslucent: true,
380
+ presentationStyle: "overFullScreen",
381
+ animationType: "none",
382
+ supportedOrientations: ['portrait', 'landscape'],
383
+ children: /*#__PURE__*/_jsx(GestureHandlerRootView, {
384
+ style: styles.modalRoot,
385
+ children: sheetTree
386
+ })
387
+ });
388
+ }
389
+ return sheetTree;
354
390
  });
355
391
  BottomSheet.displayName = 'BottomSheet';
356
392
 
@@ -390,6 +426,9 @@ const buildStyles = _theme => StyleSheet.create({
390
426
  },
391
427
  content: {
392
428
  flex: 1
429
+ },
430
+ modalRoot: {
431
+ flex: 1
393
432
  }
394
433
  });
395
434
  export { BottomSheet };
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { AccessibilityInfo, Animated, Easing, Modal, Pressable, StyleSheet, Text, View } from 'react-native';
5
- import { useTheme, createAnimatedValue } from "../../theme/index.js";
5
+ import { useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { triggerHaptic } from "../../utils/index.js";
7
7
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
8
8
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -130,8 +130,8 @@ const DatePicker = props => {
130
130
  useEffect(() => {
131
131
  if (mode !== 'modal') return;
132
132
  if (visible) {
133
- backdrop.setValue(0);
134
- sheet.setValue(0);
133
+ setNativeValue(backdrop, 0);
134
+ setNativeValue(sheet, 0);
135
135
  Animated.parallel([Animated.timing(backdrop, {
136
136
  toValue: 1,
137
137
  duration: theme.motion.duration.normal,
@@ -243,8 +243,8 @@ const DatePicker = props => {
243
243
 
244
244
  // View-mode transition: fade + scale (200ms, native driver).
245
245
  const animateViewTransition = useCallback(() => {
246
- viewFade.setValue(0);
247
- viewScale.setValue(0.9);
246
+ setNativeValue(viewFade, 0);
247
+ setNativeValue(viewScale, 0.9);
248
248
  Animated.parallel([Animated.timing(viewFade, {
249
249
  toValue: 1,
250
250
  duration: 200,
@@ -295,7 +295,7 @@ const DatePicker = props => {
295
295
  if (!cell.inMonth) {
296
296
  setAnchor(new Date(cell.date.getFullYear(), cell.date.getMonth(), 1));
297
297
  }
298
- selectScale.setValue(0.7);
298
+ setNativeValue(selectScale, 0.7);
299
299
  Animated.spring(selectScale, {
300
300
  toValue: 1,
301
301
  damping: theme.motion.spring.bouncy.damping,
@@ -377,11 +377,16 @@ const DatePicker = props => {
377
377
  opacity: viewMode === 'days' ? monthFade : 1
378
378
  }],
379
379
  children: [/*#__PURE__*/_jsx(Animated.Text, {
380
- style: [styles.headerLabel, viewMode === 'days' ? {
380
+ style: [styles.headerLabel,
381
+ // Always include the transform — passing null/undefined as a
382
+ // transform value confuses Animated's processTransform on Fabric.
383
+ // When the month slide isn't active we still drive translateX
384
+ // through the same value, just with target 0.
385
+ {
381
386
  transform: [{
382
- translateX: monthSlide
387
+ translateX: viewMode === 'days' ? monthSlide : 0
383
388
  }]
384
- } : null],
389
+ }],
385
390
  accessibilityLiveRegion: "polite",
386
391
  children: headerLabel
387
392
  }), /*#__PURE__*/_jsx(Text, {
@@ -447,11 +452,12 @@ const DatePicker = props => {
447
452
  backgroundColor: cellBg,
448
453
  borderColor,
449
454
  borderWidth: todayCell && !selected ? 1.5 : 0,
450
- opacity,
451
- transform: selected ? [{
455
+ opacity
456
+ }, selected ? {
457
+ transform: [{
452
458
  scale: selectScale
453
- }] : undefined
454
- }],
459
+ }]
460
+ } : null],
455
461
  children: /*#__PURE__*/_jsx(Text, {
456
462
  style: [styles.dayText, {
457
463
  color: textColor
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { AccessibilityInfo, Animated, Easing, Modal, Pressable, StyleSheet, Text, View } from 'react-native';
5
- import { useTheme, createAnimatedValue } from "../../theme/index.js";
5
+ import { useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { triggerHaptic } from "../../utils/index.js";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -115,8 +115,8 @@ const DateRangePicker = /*#__PURE__*/forwardRef((props, ref) => {
115
115
  // Modal open / close animation.
116
116
  useEffect(() => {
117
117
  if (visible) {
118
- backdrop.setValue(0);
119
- sheet.setValue(0);
118
+ setNativeValue(backdrop, 0);
119
+ setNativeValue(sheet, 0);
120
120
  Animated.parallel([Animated.timing(backdrop, {
121
121
  toValue: 1,
122
122
  duration: theme.motion.duration.normal,
@@ -556,26 +556,31 @@ const buildStyles = theme => {
556
556
  aspectRatio: 1,
557
557
  alignItems: 'center',
558
558
  justifyContent: 'center',
559
- padding: 2,
559
+ // No horizontal padding so the connector bars on adjacent cells meet
560
+ // without a gap. Vertical breathing room comes from dayInner's height.
561
+ padding: 0,
560
562
  position: 'relative'
561
563
  },
564
+ // Bars sit BEHIND dayInner and bridge the start/end circles to mid-range
565
+ // cells. Their height mirrors dayInner (~90% of cell height) so the visual
566
+ // pill is one continuous shape across multiple cells.
562
567
  barLeft: {
563
568
  position: 'absolute',
564
569
  left: 0,
565
570
  right: '50%',
566
- top: '15%',
567
- bottom: '15%'
571
+ top: '5%',
572
+ bottom: '5%'
568
573
  },
569
574
  barRight: {
570
575
  position: 'absolute',
571
576
  left: '50%',
572
577
  right: 0,
573
- top: '15%',
574
- bottom: '15%'
578
+ top: '5%',
579
+ bottom: '5%'
575
580
  },
576
581
  dayInner: {
577
- width: '100%',
578
- height: '100%',
582
+ width: '90%',
583
+ height: '90%',
579
584
  alignItems: 'center',
580
585
  justifyContent: 'center'
581
586
  },
@@ -3,7 +3,7 @@
3
3
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native';
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
- import { useTheme, createAnimatedValue } from "../../theme/index.js";
6
+ import { useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
7
7
  import { usePressAnimation } from "../../hooks/usePressAnimation.js";
8
8
  import { triggerHaptic } from "../../utils/hapticUtils.js";
9
9
  import { AppIcon } from "../AppIcon/index.js";
@@ -81,7 +81,7 @@ const FloatingActionButton = /*#__PURE__*/forwardRef((props, ref) => {
81
81
  const hideAnim = useRef(createAnimatedValue(0)).current;
82
82
  useEffect(() => {
83
83
  if (!hideOnScroll) {
84
- hideAnim.setValue(0);
84
+ setNativeValue(hideAnim, 0);
85
85
  return;
86
86
  }
87
87
  Animated.timing(hideAnim, {
@@ -5,7 +5,7 @@ import { Animated, Easing, Pressable, StyleSheet, Text, TextInput, View } from '
5
5
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6
6
  // @ts-ignore - react-native-vector-icons ships no bundled types in this version
7
7
  import Feather from 'react-native-vector-icons/Feather';
8
- import { createAnimatedValue, fontFor, useTheme } from "../../theme/index.js";
8
+ import { createAnimatedValue, fontFor, setNativeValue, useTheme } from "../../theme/index.js";
9
9
  import { triggerHaptic } from "../../utils/index.js";
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  const sizeMap = {
@@ -140,7 +140,7 @@ const Input = /*#__PURE__*/forwardRef((props, ref) => {
140
140
  const prevErrorRef = useRef(hasError);
141
141
  useEffect(() => {
142
142
  if (hasError && !prevErrorRef.current) {
143
- shakeAnim.setValue(0);
143
+ setNativeValue(shakeAnim, 0);
144
144
  Animated.sequence([Animated.timing(shakeAnim, {
145
145
  toValue: 1,
146
146
  duration: 50,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react';
4
4
  import { AccessibilityInfo, Animated, Dimensions, findNodeHandle, Modal as RNModal, Pressable, StyleSheet, View } from 'react-native';
5
- import { useTheme, createAnimatedValue } from "../../theme/index.js";
5
+ import { useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { triggerHaptic } from "../../utils/hapticUtils.js";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  const Modal = /*#__PURE__*/forwardRef((props, ref) => {
@@ -60,10 +60,10 @@ const Modal = /*#__PURE__*/forwardRef((props, ref) => {
60
60
  })]).start();
61
61
  }
62
62
  } else {
63
- backdropAnim.setValue(0);
64
- scaleAnim.setValue(0.9);
65
- opacityAnim.setValue(0);
66
- translateYAnim.setValue(screenHeight);
63
+ setNativeValue(backdropAnim, 0);
64
+ setNativeValue(scaleAnim, 0.9);
65
+ setNativeValue(opacityAnim, 0);
66
+ setNativeValue(translateYAnim, screenHeight);
67
67
  }
68
68
  }, [visible, presentation, duration, backdropAnim, scaleAnim, opacityAnim, translateYAnim, screenHeight, theme.motion.spring.gentle.damping, theme.motion.spring.gentle.stiffness, theme.motion.spring.gentle.mass]);
69
69
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
4
  import { Animated, Easing, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
5
- import { fontFor, useTheme, createAnimatedValue } from "../../theme/index.js";
5
+ import { fontFor, useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { triggerHaptic } from "../../utils/index.js";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  const sizeMap = {
@@ -84,7 +84,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
84
84
  const isFirstRun = previousErrorRef.current === null;
85
85
  if (!isFirstRun && hasError && !previousErrorRef.current) {
86
86
  triggerHaptic('notificationError');
87
- shake.setValue(0);
87
+ setNativeValue(shake, 0);
88
88
  Animated.sequence([Animated.timing(shake, {
89
89
  toValue: 1,
90
90
  duration: 75,
@@ -170,16 +170,27 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
170
170
  }
171
171
  }, [length, onChange, onComplete, value]);
172
172
  const handleChangeText = useCallback((index, raw) => {
173
- const sanitized = sanitizeChar(raw, keyboardType);
173
+ // Strip the ZWSP placeholder (used so iOS fires onKeyPress/Backspace on otherwise-empty cells).
174
+ const stripped = raw.replace(/\u200B/g, '');
175
+ const sanitized = sanitizeChar(stripped, keyboardType);
174
176
  if (!sanitized) {
175
- // User cleared this cell explicitly via change (rare since we manage backspace via key-press).
177
+ // Cell was cleared (delete / cut). Clear this cell and step focus back.
176
178
  const chars = cells.slice();
179
+ const wasFilled = (chars[index] ?? '').length > 0;
177
180
  chars[index] = '';
178
181
  updateValue(chars.join('').slice(0, length));
182
+ if (!wasFilled && index > 0) {
183
+ // Empty cell + backspace → also clear and focus the previous cell.
184
+ const prev = cells.slice();
185
+ prev[index - 1] = '';
186
+ updateValue(prev.join(''));
187
+ focusCell(index - 1);
188
+ }
179
189
  return;
180
190
  }
181
191
 
182
192
  // Fill cells from current index onward (handles paste of multi-char text).
193
+ // For single-character typing, this overwrites the current cell and advances.
183
194
  const chars = cells.slice();
184
195
  let writeIndex = index;
185
196
  for (let i = 0; i < sanitized.length && writeIndex < length; i += 1) {
@@ -196,7 +207,6 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
196
207
  // Move focus to the next empty cell or last cell.
197
208
  const nextFocus = Math.min(writeIndex, length - 1);
198
209
  if (writeIndex >= length) {
199
- // All filled — blur last cell.
200
210
  inputsRef.current[length - 1]?.blur();
201
211
  } else {
202
212
  focusCell(nextFocus);
@@ -205,9 +215,12 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
205
215
  const handleKeyPress = useCallback((index, e) => {
206
216
  const key = e.nativeEvent.key;
207
217
  if (key !== 'Backspace') return;
218
+ // Backspace on a non-empty cell — clear it. Backspace on an empty cell —
219
+ // step back and clear the previous cell. handleChangeText also covers the
220
+ // empty-cell case via the ZWSP placeholder, so this branch only matters
221
+ // when the platform fires onKeyPress without firing onChangeText.
208
222
  const current = cells[index] ?? '';
209
223
  if (current.length === 0) {
210
- // Move back, clear previous cell.
211
224
  if (index > 0) {
212
225
  const chars = cells.slice();
213
226
  chars[index - 1] = '';
@@ -283,8 +296,11 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
283
296
  children: [/*#__PURE__*/_jsx(TextInput, {
284
297
  ref: node => {
285
298
  inputsRef.current[index] = node;
286
- },
287
- value: secure && isFilled ? '' : char,
299
+ }
300
+ // Always render at least the ZWSP placeholder so iOS keeps
301
+ // firing onChangeText/onKeyPress for Backspace on empty cells.
302
+ ,
303
+ value: (secure && isFilled ? '' : char) || '\u200B',
288
304
  onChangeText: t => handleChangeText(index, t),
289
305
  onKeyPress: e => handleKeyPress(index, e),
290
306
  onFocus: () => handleFocus(index),
@@ -292,8 +308,12 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
292
308
  keyboardType: keyboardType,
293
309
  editable: !disabled,
294
310
  selectTextOnFocus: true,
295
- caretHidden: isFilled,
296
- maxLength: length,
311
+ caretHidden: isFilled
312
+ // Only the first cell accepts paste-like multi-char input
313
+ // (e.g., when SMS autofill or clipboard delivers the full code);
314
+ // every other cell is single-char so typing always overwrites.
315
+ ,
316
+ maxLength: index === 0 ? length : 2,
297
317
  textContentType: index === 0 ? 'oneTimeCode' : 'none',
298
318
  autoComplete: index === 0 ? 'sms-otp' : 'off',
299
319
  importantForAutofill: index === 0 ? 'yes' : 'no',
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, useEffect, useMemo, useRef } from 'react';
4
4
  import { Animated, Easing, StyleSheet, View } from 'react-native';
5
- import { useTheme, createAnimatedValue } from "../../theme/index.js";
5
+ import { useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
6
  import { jsx as _jsx } from "react/jsx-runtime";
7
7
  const toneColor = (theme, tone) => {
8
8
  switch (tone) {
@@ -62,7 +62,7 @@ const ProgressBar = /*#__PURE__*/forwardRef((props, ref) => {
62
62
  }, [animated, clamped, fillAnim, isIndeterminate, theme]);
63
63
  useEffect(() => {
64
64
  if (!isIndeterminate) return;
65
- loopAnim.setValue(0);
65
+ setNativeValue(loopAnim, 0);
66
66
  const animation = Animated.loop(Animated.timing(loopAnim, {
67
67
  toValue: 1,
68
68
  duration: 1500,