diff --git a/example/app/package.json b/example/app/package.json
index a48096da..93aba0f6 100644
--- a/example/app/package.json
+++ b/example/app/package.json
@@ -5,20 +5,25 @@
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-native-fontawesome": "0.3.2",
"@react-native-async-storage/async-storage": "2.2.0",
+ "@react-native-menu/menu": "2.0.0",
"@react-navigation/bottom-tabs": "7.3.10",
"@react-navigation/native": "7.0.14",
"@react-navigation/native-stack": "7.2.0",
"@shopify/flash-list": "2.3.1",
"react": "19.2.3",
"react-native": "0.86.0",
+ "react-native-context-menu-view": "1.21.0",
"react-native-gesture-handler": "2.32.0",
"react-native-haptic-feedback": "2.3.3",
+ "react-native-ios-context-menu": "3.1.0",
+ "react-native-ios-utilities": "5.1.2",
"react-native-reanimated": "4.5.1",
"react-native-safe-area-context": "5.7.0",
"react-native-screens": "4.25.2",
"react-native-sortables": "workspace:*",
"react-native-svg": "15.15.4",
- "react-native-worklets": "0.10.1"
+ "react-native-worklets": "0.10.1",
+ "zeego": "3.0.6"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/example/app/src/examples/SortableGrid/tests/ContextMenuExample.tsx b/example/app/src/examples/SortableGrid/tests/ContextMenuExample.tsx
new file mode 100644
index 00000000..216498a1
--- /dev/null
+++ b/example/app/src/examples/SortableGrid/tests/ContextMenuExample.tsx
@@ -0,0 +1,110 @@
+import { MenuView } from '@react-native-menu/menu';
+import { useCallback, useState } from 'react';
+import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
+import type { SortableGridRenderItem } from 'react-native-sortables';
+import Sortable, { useItemContext } from 'react-native-sortables';
+
+import { GridCard, Section } from '@/components';
+import { spacing } from '@/theme';
+import { getItems } from '@/utils';
+
+const DATA = getItems(6);
+const COLUMNS = 2;
+
+// The native menu opens on the OS long-press timeout (~500ms, not tunable), so
+// tune the sortable side: pick up sooner and treat a small move as a drag.
+const DRAG_ACTIVATION_DELAY = 120; // ms until draggable (default 200)
+// Small enough that the sortable claims the touch before the surrounding
+// ScrollView starts scrolling, but larger than the finger jitter of a
+// stationary long press (which must be left to the native context menu).
+const DRAG_FAIL_OFFSET = 8; // px of movement that counts as a drag (default 5)
+
+const MENU_ACTIONS = [
+ { id: 'edit', title: 'Edit' },
+ { attributes: { destructive: true }, id: 'delete', title: 'Delete' }
+];
+
+function noop() {
+ // Menu actions are no-ops in this example.
+}
+
+// Baseline for comparison: the native context menu on a PLAIN view (no
+// sortable). Long press it to see how the OS lifts the view and keeps it
+// scaled up while the menu is open.
+function PlainMenuCard() {
+ return (
+ // Without an explicit size the native MenuView stretches to fill the row,
+ // so the long press triggers anywhere in the container, not just on the
+ // card. Size it to the card so the hit area matches what you see.
+
+
+ Plain
+
+
+ );
+}
+
+function MenuCard({ item }: { item: string }) {
+ const { isDragging } = useItemContext();
+ const [dragging, setDragging] = useState(false);
+
+ useAnimatedReaction(
+ () => isDragging.value,
+ (value, prev) => {
+ if (value !== prev) {
+ runOnJS(setDragging)(value);
+ }
+ }
+ );
+
+ // While dragging, drop the menu so it can't open (and closes if already open).
+ // A stationary long press keeps it.
+ if (dragging) {
+ return {item};
+ }
+
+ return (
+
+ {item}
+
+ );
+}
+
+export default function ContextMenuExample() {
+ const renderItem = useCallback>(
+ ({ item }) => ,
+ []
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/example/app/src/examples/SortableGrid/tests/ContextMenuViewExample.tsx b/example/app/src/examples/SortableGrid/tests/ContextMenuViewExample.tsx
new file mode 100644
index 00000000..a213e40c
--- /dev/null
+++ b/example/app/src/examples/SortableGrid/tests/ContextMenuViewExample.tsx
@@ -0,0 +1,124 @@
+// Grid cards with a native iOS context menu (react-native-context-menu-view).
+// Long press a card and the OS lifts it and opens the menu (it stays open); a
+// drag reorders. Active drag-reorder and the native lift fundamentally fight -
+// the drag arms on the long press and the OS cancels that touch when the menu
+// commits - so the card shows a small scale-down settle as the menu opens.
+import { useCallback, useState } from 'react';
+import { View } from 'react-native';
+import ContextMenu from 'react-native-context-menu-view';
+import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
+import type { SortableGridRenderItem } from 'react-native-sortables';
+import Sortable, { useItemContext } from 'react-native-sortables';
+
+import { GridCard, Section } from '@/components';
+import { radius, spacing } from '@/theme';
+import { getItems } from '@/utils';
+
+const DATA = getItems(6);
+const COLUMNS = 2;
+
+// The native menu opens on the OS long-press timeout (~500ms, not tunable), so
+// tune the sortable side: pick up sooner and treat a small move as a drag.
+const DRAG_ACTIVATION_DELAY = 120; // ms until draggable (default 200)
+const DRAG_FAIL_OFFSET = 8; // px of movement that counts as a drag (default 5)
+
+const MENU_ACTIONS = [
+ { systemIcon: 'pencil', title: 'Edit' },
+ { destructive: true, systemIcon: 'trash', title: 'Delete' }
+];
+
+function noop() {
+ // Menu actions are no-ops in this example.
+}
+
+// Baseline (no sortable): the native menu lifts the card and keeps it scaled up
+// while the menu is open.
+function PlainMenuCard() {
+ return (
+
+
+ Plain
+
+
+ );
+}
+
+function MenuCard({ item }: { item: string }) {
+ const { isDragging } = useItemContext();
+ const [dragging, setDragging] = useState(false);
+ // Grid cards stretch to fill their cell (no explicit size). When the OS lifts
+ // such a card for the preview it has nothing to size against and collapses to
+ // nothing (the card "disappears"), so render an explicitly sized `preview`.
+ // Measure the cell on a plain wrapper View - onLayout fires reliably there -
+ // and mirror that size into the preview card.
+ const [size, setSize] = useState({ height: 170, width: 170 });
+
+ useAnimatedReaction(
+ () => isDragging.value,
+ (value, prev) => {
+ if (value !== prev) {
+ runOnJS(setDragging)(value);
+ }
+ }
+ );
+
+ return (
+ // Keep the ContextMenu mounted and just disable it while dragging - a
+ // stationary long press keeps the menu; a drag drops it (and reorders).
+
+ {item}
+
+ }
+ disableShadow
+ onPress={noop}>
+
+ setSize({ height: layout.height, width: layout.width })
+ }>
+ {item}
+
+
+ );
+}
+
+export default function ContextMenuViewExample() {
+ const renderItem = useCallback>(
+ ({ item }) => ,
+ []
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/example/app/src/examples/SortableGrid/tests/ContextMenusExample.tsx b/example/app/src/examples/SortableGrid/tests/ContextMenusExample.tsx
new file mode 100644
index 00000000..ec289526
--- /dev/null
+++ b/example/app/src/examples/SortableGrid/tests/ContextMenusExample.tsx
@@ -0,0 +1,83 @@
+import { useState } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import Sortable from 'react-native-sortables';
+
+import { ScrollScreen } from '@/components';
+import { colors, radius, spacing } from '@/theme';
+
+import ContextMenuExample from './ContextMenuExample';
+import ContextMenuViewExample from './ContextMenuViewExample';
+
+// Two ways to attach a native iOS context menu to sortable grid items, one per
+// tab. They behave the same on a long press; the difference is the library.
+const TABS = [
+ {
+ Content: ContextMenuExample,
+ key: 'menu',
+ label: '@react-native-menu/menu'
+ },
+ {
+ Content: ContextMenuViewExample,
+ key: 'view',
+ label: 'react-native-context-menu-view'
+ }
+];
+
+export default function ContextMenusExample() {
+ const [activeKey, setActiveKey] = useState(TABS[0]!.key);
+ const ActiveContent = (TABS.find(tab => tab.key === activeKey) ?? TABS[0]!)
+ .Content;
+
+ return (
+
+
+ {TABS.map(({ key, label }) => {
+ const active = key === activeKey;
+ return (
+ setActiveKey(key)}>
+
+ {label}
+
+
+ );
+ })}
+
+ {/* SortableLayer lets the dragged item render above the sections. */}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tab: {
+ alignItems: 'center',
+ backgroundColor: colors.background3,
+ borderRadius: radius.sm,
+ flex: 1,
+ paddingHorizontal: spacing.xs,
+ paddingVertical: spacing.sm
+ },
+ tabActive: {
+ backgroundColor: colors.primary
+ },
+ tabBar: {
+ flexDirection: 'row',
+ gap: spacing.xs,
+ marginBottom: spacing.md
+ },
+ tabLabel: {
+ color: colors.foreground2,
+ fontSize: 12,
+ fontWeight: '600'
+ },
+ tabLabelActive: {
+ color: colors.background1
+ }
+});
diff --git a/example/app/src/examples/SortableGrid/tests/index.ts b/example/app/src/examples/SortableGrid/tests/index.ts
index 4451576a..5109516a 100644
--- a/example/app/src/examples/SortableGrid/tests/index.ts
+++ b/example/app/src/examples/SortableGrid/tests/index.ts
@@ -1,4 +1,5 @@
export { default as BottomTabsNavigatorExample } from './BottomTabsNavigatorExample';
+export { default as ContextMenusExample } from './ContextMenusExample';
export { default as HidingItemsExample } from './HidingItemsExample';
export { default as InputFocusExample } from './InputFocusExample';
export { default as MaxOverscrollOffsetExample } from './MaxOverscrollOffsetExample';
diff --git a/example/app/src/examples/navigation/routes.ts b/example/app/src/examples/navigation/routes.ts
index ee52357d..01380aa0 100644
--- a/example/app/src/examples/navigation/routes.ts
+++ b/example/app/src/examples/navigation/routes.ts
@@ -92,6 +92,12 @@ const routes: Routes = {
Component: SortableGrid.tests.BottomTabsNavigatorExample,
name: 'Bottom Tabs Navigator'
},
+ ...(IS_IOS && {
+ ContextMenu: {
+ Component: SortableGrid.tests.ContextMenusExample,
+ name: 'Context Menu'
+ }
+ }),
HidingItems: {
Component: SortableGrid.tests.HidingItemsExample,
name: 'Hiding Items'
diff --git a/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts b/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts
index 35e26e9d..e1327894 100644
--- a/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts
+++ b/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts
@@ -100,6 +100,7 @@ const { CommonValuesContext, CommonValuesProvider, useCommonValuesContext } =
const activeAnimationProgress = useMutableValue(0);
const inactiveAnimationProgress = useMutableValue(0);
const activeItemDropped = useMutableValue(true);
+ const activeItemBroughtToFront = useMutableValue(false);
// ITEM ACTIVATION SETTINGS
const dragActivationDelay = useAnimatableValue(_dragActivationDelay);
@@ -148,6 +149,7 @@ const { CommonValuesContext, CommonValuesProvider, useCommonValuesContext } =
activationAnimationDuration,
activationState,
activeAnimationProgress,
+ activeItemBroughtToFront,
activeItemDimensions,
activeItemDropped,
activeItemKey,
diff --git a/packages/react-native-sortables/src/providers/shared/DragProvider.ts b/packages/react-native-sortables/src/providers/shared/DragProvider.ts
index a8c1702e..e278fbee 100644
--- a/packages/react-native-sortables/src/providers/shared/DragProvider.ts
+++ b/packages/react-native-sortables/src/providers/shared/DragProvider.ts
@@ -81,6 +81,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
activationAnimationDuration,
activationState,
activeAnimationProgress,
+ activeItemBroughtToFront,
activeItemDimensions,
activeItemDropped,
activeItemKey,
@@ -289,6 +290,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
activeItemPosition.value = itemPosition;
activeItemDimensions.value = itemDimensions;
activationState.value = DragActivationState.ACTIVE;
+ activeItemBroughtToFront.value = false;
ctx.dragStartIndex = keyToIndex.value[key] ?? -1;
if (activeContainerId) {
@@ -298,7 +300,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
multiZoneActiveItemDimensions.value = itemDimensions;
}
- updateLayer?.(LayerState.FOCUSED);
+ // zIndex is elevated on the first drag move (see handleTouchesMove)
// We need to update the custom handle measurements if the custom handle
// is used (touch position is relative to the handle in this case)
@@ -349,6 +351,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
activeAnimationProgress,
activeContainerId,
activeHandleOffset,
+ activeItemBroughtToFront,
activeItemDimensions,
activeItemDropped,
activationState,
@@ -366,7 +369,6 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
prevActiveItemKey,
onDragStart,
touchPosition,
- updateLayer,
updateActiveHandleMeasurements
]
);
@@ -457,6 +459,14 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
}
const ctx = context.value;
+ const startTouch = ctx.touchStartTouch;
+
+ let movedDistance = 0;
+ if (startTouch) {
+ const dX = touch.absoluteX - startTouch.absoluteX;
+ const dY = touch.absoluteY - startTouch.absoluteY;
+ movedDistance = Math.sqrt(dX * dX + dY * dY);
+ }
if (activeItemKey.value) {
onDragMove({
@@ -464,25 +474,19 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
key: activeItemKey.value,
touchData: touch
});
- }
- if (activationState.value === DragActivationState.TOUCHED) {
- if (!ctx.touchStartTouch) {
- fail();
- return;
- }
- const dX = touch.absoluteX - ctx.touchStartTouch.absoluteX;
- const dY = touch.absoluteY - ctx.touchStartTouch.absoluteY;
-
- // Cancel touch if the touch moved too far from the initial position
- // before the item activation animation starts
- const r = Math.sqrt(dX * dX + dY * dY);
+ // Elevate only once dragged, so a bare long press doesn't change the
+ // stacking context and dismiss a native context menu (#417)
if (
- // activeItemKey is set after the drag activation delay passes
- // and we don't want to cancel the touch anymore after this time
- activeItemKey.value === null &&
- r >= dragActivationFailOffset.value
+ !activeItemBroughtToFront.value &&
+ movedDistance >= dragActivationFailOffset.value
) {
+ activeItemBroughtToFront.value = true;
+ updateLayer?.(LayerState.FOCUSED);
+ }
+ } else if (activationState.value === DragActivationState.TOUCHED) {
+ // Cancel the touch if it moves before activation (it is a scroll)
+ if (!startTouch || movedDistance >= dragActivationFailOffset.value) {
fail();
return;
}
@@ -490,11 +494,13 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
},
[
activationState,
+ activeItemBroughtToFront,
activeItemKey,
dragActivationFailOffset,
currentTouch,
context,
- onDragMove
+ onDragMove,
+ updateLayer
]
);
@@ -549,7 +555,12 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
inactiveAnimationProgress.value = animate();
activeAnimationProgress.value = animate();
- updateLayer?.(LayerState.INTERMEDIATE);
+ // Restore the layer only if it was elevated. The flag resets on the next
+ // drag start, not here, to avoid racing an overlapping drag.
+ const wasBroughtToFront = activeItemBroughtToFront.value;
+ if (wasBroughtToFront) {
+ updateLayer?.(LayerState.INTERMEDIATE);
+ }
haptics.medium();
stableOnDragEnd({
@@ -562,7 +573,9 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
setAnimatedTimeout(() => {
activeItemDropped.value = true;
- updateLayer?.(LayerState.IDLE);
+ if (wasBroughtToFront) {
+ updateLayer?.(LayerState.IDLE);
+ }
onActiveItemDropped({
fromIndex,
indexToKey: indexToKey.value,
@@ -574,6 +587,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')<
},
[
activeContainerId,
+ activeItemBroughtToFront,
activeItemKey,
activeItemDimensions,
activeItemDropped,
diff --git a/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts b/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts
index 732164bf..d50e4d80 100644
--- a/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts
+++ b/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts
@@ -1,4 +1,5 @@
import type { PropsWithChildren } from 'react';
+import { useDerivedValue } from 'react-native-reanimated';
import type { ItemContextType } from '../../types';
import { createProvider } from '../utils';
@@ -20,12 +21,17 @@ const { ItemContextProvider, useItemContextContext: useItemContext } =
>(({ activationAnimationProgress, gesture, isActive, itemKey }) => {
const {
activationState,
+ activeItemBroughtToFront,
activeItemKey,
indexToKey,
keyToIndex,
prevActiveItemKey
} = useCommonValuesContext();
+ const isDragging = useDerivedValue(
+ () => isActive.value && activeItemBroughtToFront.value
+ );
+
return {
value: {
activationAnimationProgress,
@@ -34,6 +40,7 @@ const { ItemContextProvider, useItemContextContext: useItemContext } =
gesture,
indexToKey,
isActive,
+ isDragging,
itemKey,
keyToIndex,
prevActiveItemKey
diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts
index e386a262..ebe3e9e1 100644
--- a/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts
+++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts
@@ -17,6 +17,7 @@ export default function useItemDecoration(
): SharedValue {
const {
activationAnimationDuration,
+ activeItemBroughtToFront,
activeItemOpacity,
activeItemScale,
activeItemShadowOpacity,
@@ -38,8 +39,23 @@ export default function useItemDecoration(
);
});
+ // Ramp the "picked up" progress in when the item is actually brought to front
+ // (the first drag move), so the scale/shadow animate up smoothly even if the
+ // activation timing already finished during a long hold. Gating the raw
+ // progress alone would make it jump 0 -> 1 in a single frame in that case.
+ const broughtToFrontProgress = useDerivedValue(() =>
+ activeItemBroughtToFront.value
+ ? withTiming(1, { duration: activationAnimationDuration.value })
+ : 0
+ );
+
return useDerivedValue(() => {
- const progress = activationAnimationProgress.value;
+ // Apply the "picked up" scale/shadow only once the item is actually dragged,
+ // not on a bare long press (otherwise a native context menu taking over the
+ // gesture makes the item scale up and then back down). The drop animation
+ // still runs via activationAnimationProgress falling back to 0.
+ const progress =
+ broughtToFrontProgress.value * activationAnimationProgress.value;
const zeroProgressOpacity = interpolate(
adjustedInactiveProgress.value,
[0, 1],
diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts
index 8f1cf278..c9a46c79 100644
--- a/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts
+++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts
@@ -88,14 +88,30 @@ function useItemLayoutFabric(
position: SharedValue,
zIndex: SharedValue
): SharedValue {
- const { activeItemDropped, usesAbsoluteLayout } = useCommonValuesContext();
+ const { activeItemBroughtToFront, activeItemDropped, usesAbsoluteLayout } =
+ useCommonValuesContext();
const transformStartPosition = useMutableValue(null);
const debounce = useAnimatedDebounce();
useAnimatedReaction(
- () => ({ current: position.value, dropped: activeItemDropped.value }),
- ({ current, dropped }) => {
- transformStartPosition.value ??= current;
+ () => ({
+ broughtToFront: activeItemBroughtToFront.value,
+ current: position.value,
+ dropped: activeItemDropped.value
+ }),
+ ({ broughtToFront, current, dropped }) => {
+ // Only switch to transform-based positioning once the item is actually
+ // dragged (brought to front). A bare long press that never turns into a
+ // drag then stays on a stable left/top-only style and never toggles the
+ // transform<->layout shape, so it never re-commits a frame to the item
+ // subtree. That matters on Fabric because a frame/onLayout on the item
+ // while a native context menu is lifting it (the menu portals the live
+ // view) invalidates the lift and snaps the item back into place. For a
+ // stationary item the transform deltas are always 0, so skipping the
+ // transform branch is visually identical.
+ if (broughtToFront) {
+ transformStartPosition.value ??= current;
+ }
if (dropped) {
debounce.schedule(() => {
transformStartPosition.value = null;
diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.test.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.test.ts
new file mode 100644
index 00000000..b92ab706
--- /dev/null
+++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.test.ts
@@ -0,0 +1,130 @@
+import { renderHook } from '@testing-library/react-hooks';
+import type { SharedValue } from 'react-native-reanimated';
+import { makeMutable } from 'react-native-reanimated';
+
+import { useCommonValuesContext } from '../CommonValuesProvider';
+import useItemZIndex from './useItemZIndex';
+
+jest.mock('../CommonValuesProvider', () => ({
+ useCommonValuesContext: jest.fn()
+}));
+
+const mockedUseCommonValuesContext = useCommonValuesContext as jest.Mock;
+
+type ContextOverrides = {
+ activeItemBroughtToFront?: boolean;
+ activeItemKey?: null | string;
+ prevActiveItemKey?: null | string;
+ isStackingOrderDesc?: boolean;
+ indexToKey?: Array;
+};
+
+function setupContext({
+ activeItemBroughtToFront = false,
+ activeItemKey = null,
+ indexToKey = ['a', 'b', 'c'],
+ isStackingOrderDesc = false,
+ prevActiveItemKey = null
+}: ContextOverrides) {
+ const keyToIndex = Object.fromEntries(indexToKey.map((key, i) => [key, i]));
+ mockedUseCommonValuesContext.mockReturnValue({
+ activeItemBroughtToFront: makeMutable(activeItemBroughtToFront),
+ activeItemKey: makeMutable(activeItemKey),
+ indexToKey: makeMutable(indexToKey),
+ isStackingOrderDesc,
+ keyToIndex: makeMutable(keyToIndex),
+ prevActiveItemKey: makeMutable(prevActiveItemKey)
+ });
+}
+
+function renderZIndex(key: string, activationProgress: number): number {
+ const progress = makeMutable(activationProgress);
+ const { result } = renderHook(() => useItemZIndex(key, progress)) as {
+ result: { current: SharedValue };
+ };
+ return result.current.value;
+}
+
+// itemCount = 3 in all cases below
+
+describe(useItemZIndex, () => {
+ describe('when the active item has NOT been brought to front (long press)', () => {
+ it('keeps the active item at its plain order zIndex', () => {
+ setupContext({
+ activeItemBroughtToFront: false,
+ activeItemKey: 'a'
+ });
+ // active + activation running, but not brought to front => order zIndex
+ expect(renderZIndex('a', 1)).toBe(0);
+ });
+
+ it('keeps inactive items at their plain order zIndex', () => {
+ setupContext({
+ activeItemBroughtToFront: false,
+ activeItemKey: 'a'
+ });
+ expect(renderZIndex('b', 1)).toBe(1);
+ expect(renderZIndex('c', 1)).toBe(2);
+ });
+
+ it('respects descending stacking order', () => {
+ setupContext({
+ activeItemBroughtToFront: false,
+ activeItemKey: 'a',
+ isStackingOrderDesc: true
+ });
+ // desc order zIndex for index 0 of 3 items => 3 - 0 - 1 = 2
+ expect(renderZIndex('a', 1)).toBe(2);
+ });
+ });
+
+ describe('when the active item HAS been brought to front (real drag)', () => {
+ it('elevates the active item above everything', () => {
+ setupContext({
+ activeItemBroughtToFront: true,
+ activeItemKey: 'a'
+ });
+ // 2 * itemCount + 1 = 7
+ expect(renderZIndex('a', 1)).toBe(7);
+ });
+
+ it('lifts inactive items by itemCount while the activation runs', () => {
+ setupContext({
+ activeItemBroughtToFront: true,
+ activeItemKey: 'a'
+ });
+ // itemCount + orderZIndex
+ expect(renderZIndex('b', 1)).toBe(4);
+ expect(renderZIndex('c', 1)).toBe(5);
+ });
+
+ it('keeps the previously active item above siblings during the drop', () => {
+ setupContext({
+ activeItemBroughtToFront: true,
+ activeItemKey: null,
+ prevActiveItemKey: 'a'
+ });
+ // 2 * itemCount = 6 for the dropping item
+ expect(renderZIndex('a', 0.5)).toBe(6);
+ // other items are lifted by itemCount while the drop animates
+ expect(renderZIndex('b', 0.5)).toBe(4);
+ });
+ });
+
+ describe('at rest (no active item, animation finished)', () => {
+ it.each([false, true])(
+ 'returns the plain order zIndex (broughtToFront=%s)',
+ broughtToFront => {
+ setupContext({
+ activeItemBroughtToFront: broughtToFront,
+ activeItemKey: null,
+ prevActiveItemKey: 'a'
+ });
+ // a stale broughtToFront=true is harmless at rest
+ expect(renderZIndex('a', 0)).toBe(0);
+ expect(renderZIndex('b', 0)).toBe(1);
+ expect(renderZIndex('c', 0)).toBe(2);
+ }
+ );
+ });
+});
diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts
index fc411a13..985a3567 100644
--- a/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts
+++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts
@@ -8,6 +8,7 @@ export default function useItemZIndex(
activationAnimationProgress: SharedValue
): SharedValue {
const {
+ activeItemBroughtToFront,
activeItemKey,
indexToKey,
isStackingOrderDesc,
@@ -17,16 +18,20 @@ export default function useItemZIndex(
return useDerivedValue(() => {
const itemCount = indexToKey.value.length;
-
- if (activeItemKey.value === key) {
- return 2 * itemCount + 1;
- }
-
const realIndex = keyToIndex.value[key] ?? 0;
const orderZIndex = isStackingOrderDesc
? itemCount - realIndex - 1
: realIndex;
+ // Keep the order zIndex until the item is dragged (#417)
+ if (!activeItemBroughtToFront.value) {
+ return orderZIndex;
+ }
+
+ if (activeItemKey.value === key) {
+ return 2 * itemCount + 1;
+ }
+
if (activationAnimationProgress.value > 0) {
if (prevActiveItemKey.value === key) {
return 2 * itemCount;
diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts
index 5c4bcc9a..9a126762 100644
--- a/packages/react-native-sortables/src/types/providers/shared.ts
+++ b/packages/react-native-sortables/src/types/providers/shared.ts
@@ -87,6 +87,8 @@ export type CommonValuesContextType =
activeAnimationProgress: SharedValue;
inactiveAnimationProgress: SharedValue;
activeItemDropped: SharedValue;
+ // True once the active item is dragged (not just long pressed)
+ activeItemBroughtToFront: SharedValue;
// OTHER
containerRef: AnimatedRef;
@@ -154,6 +156,9 @@ export type ItemContextType = Simplify<
Pick & {
itemKey: string;
isActive: SharedValue;
+ // True once the active item is dragged (moved), not just long pressed.
+ // Use it to disable interactive children while dragging.
+ isDragging: SharedValue;
activationAnimationProgress: SharedValue;
}
> & {
diff --git a/yarn.lock b/yarn.lock
index e887683b..8878a367 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1975,6 +1975,13 @@ __metadata:
languageName: node
linkType: hard
+"@dominicstop/ts-event-emitter@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@dominicstop/ts-event-emitter@npm:1.1.0"
+ checksum: 10c0/fe16c03132736a0a798c232636ab6b127fd8063509511ccafbd99405c224a106cfb7a8d4af13c4f6f412ec0ea3baae3678aa2c8c09d81c9985c423d476404e1a
+ languageName: node
+ linkType: hard
+
"@effect/schema@npm:0.69.0":
version: 0.69.0
resolution: "@effect/schema@npm:0.69.0"
@@ -2641,6 +2648,44 @@ __metadata:
languageName: node
linkType: hard
+"@floating-ui/core@npm:^1.7.5":
+ version: 1.7.5
+ resolution: "@floating-ui/core@npm:1.7.5"
+ dependencies:
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d
+ languageName: node
+ linkType: hard
+
+"@floating-ui/dom@npm:^1.7.6":
+ version: 1.7.6
+ resolution: "@floating-ui/dom@npm:1.7.6"
+ dependencies:
+ "@floating-ui/core": "npm:^1.7.5"
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10c0/5c098e0d7b58c9bc769f276cca1766994c2c9c70c92d091a61bba8b3e9be53c011e0a79a8457fc2fb2f3d91697a26eb52e0a4962ef936dc963b45f58613c212f
+ languageName: node
+ linkType: hard
+
+"@floating-ui/react-dom@npm:^2.0.0":
+ version: 2.1.8
+ resolution: "@floating-ui/react-dom@npm:2.1.8"
+ dependencies:
+ "@floating-ui/dom": "npm:^1.7.6"
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+ checksum: 10c0/26260ca4bb23b57c73b824062505abf977a008ce6e0463bdacca74f7e49853c4cd1d2bbf1a77c6caa17fa37dfffda2c6c4cd07a8737ebd7474aaff7818401d75
+ languageName: node
+ linkType: hard
+
+"@floating-ui/utils@npm:^0.2.11":
+ version: 0.2.11
+ resolution: "@floating-ui/utils@npm:0.2.11"
+ checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3
+ languageName: node
+ linkType: hard
+
"@fortawesome/fontawesome-common-types@npm:6.5.2":
version: 6.5.2
resolution: "@fortawesome/fontawesome-common-types@npm:6.5.2"
@@ -3604,6 +3649,471 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/primitive@npm:1.1.4":
+ version: 1.1.4
+ resolution: "@radix-ui/primitive@npm:1.1.4"
+ checksum: 10c0/f366fa15b721a466ad78f9b5b966f7129fb6932f6f33ab117253ce8c6a13f4895dce4a1fd483d3f15859623d3bfd964a4b172fe1d6ccc3a1a3c4de4c2b861119
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-arrow@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-arrow@npm:1.1.11"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/d8db1fc66022057fbdb02f4a20c747706f9b9dc7e6966bd83a0c7d1aa81f27cbcfa48deb9bd86c2714f16d9c813868240bb584473e3344beb436ce4e03f6bbaf
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-collection@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-collection@npm:1.1.11"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.4"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-slot": "npm:1.3.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/529325b57e5ec347c91d19e23e94bb0a78d01f8cf1f4f74bfc7683467656bb05f82b60e51937e7b17c89d58a129170e6825ce383dfc8408a1de9ae120cbdfe53
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-compose-refs@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@radix-ui/react-compose-refs@npm:1.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/c4853e7bc15cda6f68a1bbd7910412cc7f298419ee363fb4e40494e16a1cfadc857b2384dbe4d98b58c1312f280ca3474f67dc14da325d29ae8b7ebcfddaf743
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-context-menu@npm:^2.0.1":
+ version: 2.3.2
+ resolution: "@radix-ui/react-context-menu@npm:2.3.2"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.4"
+ "@radix-ui/react-context": "npm:1.1.4"
+ "@radix-ui/react-menu": "npm:2.1.19"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/ae423e564cdf03412e89ea4fb9b50c3b15a7b5048908b431ff3787744abb4f6ea72c9053c48ef36cb5cfeac10e6731da64d9170b2d18ef308d7b8762f0669cc0
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-context@npm:1.1.4":
+ version: 1.1.4
+ resolution: "@radix-ui/react-context@npm:1.1.4"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/917be4dcb9fd9f465ab18f6982879daf83a5ca298a22504fa3bc4abd8dea963a57abb9ad38c099dd756ba2cddff0edde680bf8369012f44dedede20912702c8f
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-direction@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-direction@npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/e8172f8598b5bddaf82bdc2e16e9561d0d89eaf85ef3cd1fe2506a1564398ee6cb07b22fd7d0ac892e733d4f6247b1e328c4b3680141d22a04e262f14ff11230
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-dismissable-layer@npm:1.1.14":
+ version: 1.1.14
+ resolution: "@radix-ui/react-dismissable-layer@npm:1.1.14"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.4"
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/371ec716e2029c503739a9efe2f85afca8d158474023a670fc546a1104824c5d78fcaba1484c652832fa006050fde2a8b52c851cd41d02cf8d564d8937aec027
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-dropdown-menu@npm:^2.0.1":
+ version: 2.1.19
+ resolution: "@radix-ui/react-dropdown-menu@npm:2.1.19"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.4"
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.4"
+ "@radix-ui/react-id": "npm:1.1.2"
+ "@radix-ui/react-menu": "npm:2.1.19"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/901a290f3542163403dd6d5439cc43c37ee8e8b3949e3b017784660f6c5bf477165d9fb5f483f52fd5914d72aa84275778312502257ce045b8c6fd553f758234
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-guards@npm:1.1.4":
+ version: 1.1.4
+ resolution: "@radix-ui/react-focus-guards@npm:1.1.4"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/0447a97a2672ad04fa73e0af3a09adafa1e85327c43cec2ae7267daa8544c9e6ef3136fbadcbdd3e28bf1ff0864537241c159e26cf5800b7698e5e69772e7ed3
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-scope@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-focus-scope@npm:1.1.11"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/e773dfbe4c3907067db4d96082bf9a4dad730ad7396dc181f09fc79e054c4a7382dee6a3b948efef97904ae0387dc81b7733e479af69f5a22de2649c99847561
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-id@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-id@npm:1.1.2"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/cd8638f0e259b9cee26cc8248b96533927abf91302a9164d674a45119a15dbb6929652e450c18bc3b99adf5d02a698152bf45d3ae052e1cd63f68fe8dd1ca87d
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-menu@npm:2.1.19":
+ version: 2.1.19
+ resolution: "@radix-ui/react-menu@npm:2.1.19"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.4"
+ "@radix-ui/react-collection": "npm:1.1.11"
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.4"
+ "@radix-ui/react-direction": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.14"
+ "@radix-ui/react-focus-guards": "npm:1.1.4"
+ "@radix-ui/react-focus-scope": "npm:1.1.11"
+ "@radix-ui/react-id": "npm:1.1.2"
+ "@radix-ui/react-popper": "npm:1.3.2"
+ "@radix-ui/react-portal": "npm:1.1.13"
+ "@radix-ui/react-presence": "npm:1.1.6"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-roving-focus": "npm:1.1.14"
+ "@radix-ui/react-slot": "npm:1.3.0"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.2"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.7.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/25a625a2d0e08b60ce29c015f19c582e2729f25321c07b03b9b2ffe0a538628fbea50353f9d447077c3c224b09c21cda64d6e24e8cbbdb52d041b5cd6b0253ca
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-popper@npm:1.3.2":
+ version: 1.3.2
+ resolution: "@radix-ui/react-popper@npm:1.3.2"
+ dependencies:
+ "@floating-ui/react-dom": "npm:^2.0.0"
+ "@radix-ui/react-arrow": "npm:1.1.11"
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.4"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ "@radix-ui/react-use-rect": "npm:1.1.2"
+ "@radix-ui/react-use-size": "npm:1.1.2"
+ "@radix-ui/rect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/84d6ed24c0fe36acecd09b7b8c3343eaa2bdc7500cf28dec18c8236d4ebb12d7df48df2b866232bd035ccbc42be57fff1144454ae9cab6a6a3a75b073e9d022d
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-portal@npm:1.1.13":
+ version: 1.1.13
+ resolution: "@radix-ui/react-portal@npm:1.1.13"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/3a158e4e9c4360dec0ff4303be2d3eead0c4438c8c240194a6116901cd8fcf98e22bcd65f33caaa778556cc434a5048b16a8deea82b60eb8f487f36f67fecf06
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-presence@npm:1.1.6":
+ version: 1.1.6
+ resolution: "@radix-ui/react-presence@npm:1.1.6"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/8b96ba32eae20162005f7e818b07e1e6e351da03f4753a79403ec58d27c4965e881a506960283fd7ded5b8ecf5ccb6a58bbc1d6799f5254a6cd4e5ae8837e345
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-primitive@npm:2.1.7":
+ version: 2.1.7
+ resolution: "@radix-ui/react-primitive@npm:2.1.7"
+ dependencies:
+ "@radix-ui/react-slot": "npm:1.3.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/0cb8711df46447966f32752ca10237a635a5207e75e73f716cc740b418aa4a1089fce342c9741e3c727274468dea205cee3b8a4433a80100bfdb624b7ba4ade6
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-roving-focus@npm:1.1.14":
+ version: 1.1.14
+ resolution: "@radix-ui/react-roving-focus@npm:1.1.14"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.4"
+ "@radix-ui/react-collection": "npm:1.1.11"
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.4"
+ "@radix-ui/react-direction": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.7"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.2"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/76be4ec8ba0c86b6115be02d2b5377af78f2871a323199887f1746ae5e7937656e9e4a79ceff08f4ee0984a222de5b343444d989e424ea56cf1a2a52cd8a6969
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-slot@npm:1.3.0":
+ version: 1.3.0
+ resolution: "@radix-ui/react-slot@npm:1.3.0"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/7353520950dcbc5c331f79aeac5bc8efa5130014e8121f85b337996ec5a2cef89f1375ff879b72ba05182de93c1126b22ec328f171edd7a2888c1532d10f74b5
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-callback-ref@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-use-callback-ref@npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/aa43df8cf54b410f2b0fff817247cee5e4d4560b86d9bff7c17c19441726163c28f09f908e154607e5e6d0a055538c768381b723c9f5d1019807546f352b4cc1
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-controllable-state@npm:1.2.3":
+ version: 1.2.3
+ resolution: "@radix-ui/react-use-controllable-state@npm:1.2.3"
+ dependencies:
+ "@radix-ui/react-use-effect-event": "npm:0.0.3"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/8c551263a2753cebc1d60019d8beb307d20cbb1b9847887a9eed13db5392672c6d5179b7f46a2cbd34a64bda702617670012ebcd32cffeb22f6645fcf5345ee8
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-effect-event@npm:0.0.3":
+ version: 0.0.3
+ resolution: "@radix-ui/react-use-effect-event@npm:0.0.3"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/8808fab0b26e30da9b50070a426b3ac135132c360b23a2a5de331bd06d8b164b10cd5561573dd17530172ba67dde428057fbf9a1f5fae2cba3cc66f118e72406
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-layout-effect@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-use-layout-effect@npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/c7fc76744a4d37b5010eac33eda6439db2ac4d657e2bb5f596236e60aa4384ac88a7b2e3ebc1c88312c5c536a40081cc85ae6d70c0bba4bb8702d302b8ff07f7
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-rect@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-use-rect@npm:1.1.2"
+ dependencies:
+ "@radix-ui/rect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/eafaa88ea33bfb3761a81a3badb73ecf99c613aa7c10e53dac08fee3457d21f4dfd1e65b5a47e7925e4aa1da60f2793404183353d4d21e85aae4009c76de64d4
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-size@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-use-size@npm:1.1.2"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/0a088412bb831fd1d59f9058352aca384c2d0a07b894b35707b3486eea4a1d8a37a04fb117379f9a8a46a921b0092f97b29b517689c8f97029ec73eb68973521
+ languageName: node
+ linkType: hard
+
+"@radix-ui/rect@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/rect@npm:1.1.2"
+ checksum: 10c0/304d648c4b6786755a10eabf2327f40b7c6303bb588174ab6194a5bb032c195c51f3e43395dee1dd37239fa68b63558092f2bc8ce5a14962d5e4303afb0a17ca
+ languageName: node
+ linkType: hard
+
"@react-native-async-storage/async-storage@npm:2.2.0":
version: 2.2.0
resolution: "@react-native-async-storage/async-storage@npm:2.2.0"
@@ -3793,6 +4303,16 @@ __metadata:
languageName: node
linkType: hard
+"@react-native-menu/menu@npm:2.0.0":
+ version: 2.0.0
+ resolution: "@react-native-menu/menu@npm:2.0.0"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 10c0/1e88e1892730c70e28a654a8df6427f80f1b88880eaa62f2744072b9cebead4e834490d15ef7f0b91eadf9cb1ec9241b29f116ce7e94661685b41faf10af3fa3
+ languageName: node
+ linkType: hard
+
"@react-native/assets-registry@npm:0.81.4":
version: 0.81.4
resolution: "@react-native/assets-registry@npm:0.81.4"
@@ -5577,6 +6097,15 @@ __metadata:
languageName: node
linkType: hard
+"aria-hidden@npm:^1.2.4":
+ version: 1.2.6
+ resolution: "aria-hidden@npm:1.2.6"
+ dependencies:
+ tslib: "npm:^2.0.0"
+ checksum: 10c0/7720cb539497a9f760f68f98a4b30f22c6767aa0e72fa7d58279f7c164e258fc38b2699828f8de881aab0fc8e9c56d1313a3f1a965046fc0381a554dbc72b54a
+ languageName: node
+ linkType: hard
+
"arktype@npm:^2.1.15":
version: 2.1.20
resolution: "arktype@npm:2.1.20"
@@ -7394,6 +7923,13 @@ __metadata:
languageName: node
linkType: hard
+"detect-node-es@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "detect-node-es@npm:1.1.0"
+ checksum: 10c0/e562f00de23f10c27d7119e1af0e7388407eb4b06596a25f6d79a360094a109ff285de317f02b090faae093d314cf6e73ac3214f8a5bb3a0def5bece94557fbe
+ languageName: node
+ linkType: hard
+
"detective-amd@npm:^6.0.0":
version: 6.0.0
resolution: "detective-amd@npm:6.0.0"
@@ -8439,6 +8975,7 @@ __metadata:
"@react-native-community/cli": "npm:20.2.0"
"@react-native-community/cli-platform-android": "npm:20.2.0"
"@react-native-community/cli-platform-ios": "npm:20.2.0"
+ "@react-native-menu/menu": "npm:2.0.0"
"@react-native/babel-preset": "npm:0.86.0"
"@react-native/jest-preset": "npm:0.86.0"
"@react-native/metro-config": "npm:0.86.0"
@@ -8458,8 +8995,11 @@ __metadata:
prettier: "npm:^3.3.2"
react: "npm:19.2.3"
react-native: "npm:0.86.0"
+ react-native-context-menu-view: "npm:1.21.0"
react-native-gesture-handler: "npm:2.32.0"
react-native-haptic-feedback: "npm:2.3.3"
+ react-native-ios-context-menu: "npm:3.1.0"
+ react-native-ios-utilities: "npm:5.1.2"
react-native-reanimated: "npm:4.5.1"
react-native-safe-area-context: "npm:5.7.0"
react-native-screens: "npm:4.25.2"
@@ -8468,6 +9008,7 @@ __metadata:
react-native-worklets: "npm:0.10.1"
react-test-renderer: "npm:19.2.3"
typescript: "npm:^5.8.3"
+ zeego: "npm:3.0.6"
languageName: unknown
linkType: soft
@@ -9363,6 +9904,13 @@ __metadata:
languageName: node
linkType: hard
+"get-nonce@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "get-nonce@npm:1.0.1"
+ checksum: 10c0/2d7df55279060bf0568549e1ffc9b84bc32a32b7541675ca092dce56317cdd1a59a98dcc4072c9f6a980779440139a3221d7486f52c488e69dc0fd27b1efb162
+ languageName: node
+ linkType: hard
+
"get-own-enumerable-property-symbols@npm:^3.0.0":
version: 3.0.2
resolution: "get-own-enumerable-property-symbols@npm:3.0.2"
@@ -14888,6 +15436,16 @@ __metadata:
languageName: node
linkType: hard
+"react-native-context-menu-view@npm:1.21.0":
+ version: 1.21.0
+ resolution: "react-native-context-menu-view@npm:1.21.0"
+ peerDependencies:
+ react: ^16.8.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-native: ">=0.60.0-rc.0 <1.0.x"
+ checksum: 10c0/923a11c034da814ac24402af0308b219a319f8b22d9a0ca381cf820f3f863c15bc08fa3c6107cdc7127004f5ba0096374a764b511edb664acb53a9212b68ae26
+ languageName: node
+ linkType: hard
+
"react-native-gesture-handler@npm:2.28.0":
version: 2.28.0
resolution: "react-native-gesture-handler@npm:2.28.0"
@@ -14939,6 +15497,29 @@ __metadata:
languageName: node
linkType: hard
+"react-native-ios-context-menu@npm:3.1.0":
+ version: 3.1.0
+ resolution: "react-native-ios-context-menu@npm:3.1.0"
+ dependencies:
+ "@dominicstop/ts-event-emitter": "npm:^1.1.0"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ react-native-ios-utilities: "*"
+ checksum: 10c0/7dda8d8ee2b0871b9c1bfce985bce49b8762988b34c5af6679fcdf47f125e98df7c1159245f7ca4070a00fd9874336a4fa17f8bf582377e849a850eb511d7680
+ languageName: node
+ linkType: hard
+
+"react-native-ios-utilities@npm:5.1.2":
+ version: 5.1.2
+ resolution: "react-native-ios-utilities@npm:5.1.2"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 10c0/b1b700faa2f447423cbfbe941a5e0eaeb7357df5f85f0a5d27a717f040174ecc3f972007f39e76cd053e4d90248028b137726524d71756e3a051efa32136754d
+ languageName: node
+ linkType: hard
+
"react-native-is-edge-to-edge@npm:1.1.7":
version: 1.1.7
resolution: "react-native-is-edge-to-edge@npm:1.1.7"
@@ -15321,6 +15902,57 @@ __metadata:
languageName: node
linkType: hard
+"react-remove-scroll-bar@npm:^2.3.7":
+ version: 2.3.8
+ resolution: "react-remove-scroll-bar@npm:2.3.8"
+ dependencies:
+ react-style-singleton: "npm:^2.2.2"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/9a0675c66cbb52c325bdbfaed80987a829c4504cefd8ff2dd3b6b3afc9a1500b8ec57b212e92c1fb654396d07bbe18830a8146fe77677d2a29ce40b5e1f78654
+ languageName: node
+ linkType: hard
+
+"react-remove-scroll@npm:^2.7.2":
+ version: 2.7.2
+ resolution: "react-remove-scroll@npm:2.7.2"
+ dependencies:
+ react-remove-scroll-bar: "npm:^2.3.7"
+ react-style-singleton: "npm:^2.2.3"
+ tslib: "npm:^2.1.0"
+ use-callback-ref: "npm:^1.3.3"
+ use-sidecar: "npm:^1.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/b5f3315bead75e72853f492c0b51ba8fb4fa09a4534d7fc42d6fcd59ca3e047cf213279ffc1e35b337e314ef5a04cb2b12544c85e0078802271731c27c09e5aa
+ languageName: node
+ linkType: hard
+
+"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3":
+ version: 2.2.3
+ resolution: "react-style-singleton@npm:2.2.3"
+ dependencies:
+ get-nonce: "npm:^1.0.0"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/841938ff16d16a6b76895f4cb2e1fea957e5fe3b30febbf03a54892dae1c9153f2383e231dea0b3ba41192ad2f2849448fa859caccd288943bce32639e971bee
+ languageName: node
+ linkType: hard
+
"react-test-renderer@npm:19.2.3":
version: 19.2.3
resolution: "react-test-renderer@npm:19.2.3"
@@ -16219,6 +16851,13 @@ __metadata:
languageName: node
linkType: hard
+"sf-symbols-typescript@npm:^2.0.0":
+ version: 2.2.0
+ resolution: "sf-symbols-typescript@npm:2.2.0"
+ checksum: 10c0/3f3bbf33aaad19e619d6f169899b39e9fe9c5fd21f0d6d511100e36887606ad349109ddc6ff82933f2b8cbf437dd7105c2ae6b0059b291dc47f143b30c2074cc
+ languageName: node
+ linkType: hard
+
"shebang-command@npm:^2.0.0":
version: 2.0.0
resolution: "shebang-command@npm:2.0.0"
@@ -17468,7 +18107,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:2.8.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
+"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@@ -17916,6 +18555,21 @@ __metadata:
languageName: node
linkType: hard
+"use-callback-ref@npm:^1.3.3":
+ version: 1.3.3
+ resolution: "use-callback-ref@npm:1.3.3"
+ dependencies:
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/f887488c6e6075cdad4962979da1714b217bcb1ee009a9e57ce9a844bcfc4c3a99e93983dfc2e5af9e0913824d24e730090ff255e902c516dcb58d2d3837e01c
+ languageName: node
+ linkType: hard
+
"use-latest-callback@npm:^0.2.1":
version: 0.2.1
resolution: "use-latest-callback@npm:0.2.1"
@@ -17925,6 +18579,22 @@ __metadata:
languageName: node
linkType: hard
+"use-sidecar@npm:^1.1.3":
+ version: 1.1.3
+ resolution: "use-sidecar@npm:1.1.3"
+ dependencies:
+ detect-node-es: "npm:^1.1.0"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/161599bf921cfaa41c85d2b01c871975ee99260f3e874c2d41c05890d41170297bdcf314bc5185e7a700de2034ac5b888e3efc8e9f35724f4918f53538d717c9
+ languageName: node
+ linkType: hard
+
"use-sync-external-store@npm:^1.2.2":
version: 1.4.0
resolution: "use-sync-external-store@npm:1.4.0"
@@ -18485,6 +19155,23 @@ __metadata:
languageName: node
linkType: hard
+"zeego@npm:3.0.6":
+ version: 3.0.6
+ resolution: "zeego@npm:3.0.6"
+ dependencies:
+ "@radix-ui/react-context-menu": "npm:^2.0.1"
+ "@radix-ui/react-dropdown-menu": "npm:^2.0.1"
+ sf-symbols-typescript: "npm:^2.0.0"
+ peerDependencies:
+ "@react-native-menu/menu": 1.2.2
+ react: "*"
+ react-native: "*"
+ react-native-ios-context-menu: 3.1.0
+ react-native-ios-utilities: 5.1.2
+ checksum: 10c0/76eec9ca0cee085da0bb50188ee08d6d67e3b2b2ea17b0a30e34fb3e39cd67a75d2650bdd4f5c593aec89cd778622d083dd20c61fd6adebe5ca01d694ac30fd5
+ languageName: node
+ linkType: hard
+
"zod-validation-error@npm:^3.0.3":
version: 3.5.0
resolution: "zod-validation-error@npm:3.5.0"