diff --git a/.changeset/dismiss-direction-interaction.md b/.changeset/dismiss-direction-interaction.md
new file mode 100644
index 0000000..51c417e
--- /dev/null
+++ b/.changeset/dismiss-direction-interaction.md
@@ -0,0 +1,17 @@
+---
+'@react-native-motion-kit/swipe-deck': minor
+---
+
+Add `interaction.dismissDirection` to `useDeckInteraction()` so UI-thread consumers can read the accepted dismiss side without waiting for JS swipe events or inferring from raw drag direction.
+
+```tsx
+function DeckDismissFeedback() {
+ const { dismissDirection, phase } = ProfileDeck.useDeckInteraction();
+
+ const rightStyle = useAnimatedStyle(() => ({
+ opacity: phase.get() === 'dismissing' && dismissDirection.get() === 'right' ? 1 : 0,
+ }));
+
+ return ;
+}
+```
diff --git a/README.ko.md b/README.ko.md
index 53103be..8d6c698 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -272,8 +272,13 @@ function ProfileDeckScreen() {
`useDeckEvent` / `useDeckEventListener`를 사용하세요.
- `dismissing`은 dismiss가 accepted된 뒤 다음 item commit과 interaction reset이 끝날 때까지의
lifecycle을 포함합니다. 즉, 화면 밖으로 나가는 animation frame만 의미하지는 않습니다.
+ - `interaction.direction`은 raw live drag/dismiss signal입니다(`-1 | 0 | 1`).
+ 외부 UI가 UI thread에서 accepted dismiss 방향(`left | right | null`)을 알아야 한다면
+ `interaction.dismissDirection`을 사용하세요. `dismissDirection`은 lifecycle state이지,
+ commit된 `swipe` event payload가 아닙니다.
- programmatic springboard action은 action이 받아들여지는 즉시 `dismissing`으로 들어갑니다.
- anticipation 중에는 실제 dismiss phase가 시작되기 전까지 `interaction.direction`이 neutral일 수 있습니다.
+ anticipation 중에는 실제 dismiss phase가 시작되기 전까지 `interaction.direction`이 neutral일 수 있지만,
+ `interaction.dismissDirection`에는 이미 accepted action direction이 들어있습니다.
- `useDeckEvent(eventName, initialValue?, id?)`는 React 렌더링용 최신 commit event를 반환합니다.
첫 event 전과 deck이 detach된 뒤에는 `undefined` 또는 `initialValue`를 반환합니다.
- `useDeckEventListener(eventName, listener, id?)`는 앱 코드에서 별도 state를 만들지 않고 commit된 model event를 구독합니다.
diff --git a/README.md b/README.md
index acbc4d4..21554ff 100644
--- a/README.md
+++ b/README.md
@@ -277,8 +277,13 @@ function ProfileDeckScreen() {
- `dismissing` covers the accepted dismiss lifecycle until the deck has committed the next item
and reset interaction values. It is intentionally broader than only the offscreen animation
frames.
+ - `interaction.direction` is the raw live drag/dismiss signal (`-1 | 0 | 1`). Use
+ `interaction.dismissDirection` when external UI needs the accepted dismiss side
+ (`left | right | null`) on the UI thread. `dismissDirection` is lifecycle state, not the
+ committed `swipe` event payload.
- Programmatic springboard actions enter `dismissing` as soon as the action is accepted. During
- anticipation, `interaction.direction` can still be neutral until the real dismiss phase starts.
+ anticipation, `interaction.direction` can still be neutral until the real dismiss phase starts,
+ while `interaction.dismissDirection` already contains the accepted action direction.
- `useDeckEvent(eventName, initialValue?, id?)` returns the latest committed deck event for
React-rendered UI. It returns `undefined` or `initialValue` before the first event and after the
deck detaches.
diff --git a/example/App.tsx b/example/App.tsx
index 43a167a..c60eb10 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -97,7 +97,7 @@ function SwipeReactionOverlay() {
}
function DeckPhaseFeedback() {
- const { phase } = ProfileDeck.useDeckInteraction();
+ const { dismissDirection, phase } = ProfileDeck.useDeckInteraction();
const idleStyle = useAnimatedStyle(() => {
return {
@@ -123,6 +123,18 @@ function DeckPhaseFeedback() {
};
});
+ const leftDismissStyle = useAnimatedStyle(() => {
+ return {
+ opacity: dismissDirection.get() === 'left' ? 1 : 0.24,
+ };
+ });
+
+ const rightDismissStyle = useAnimatedStyle(() => {
+ return {
+ opacity: dismissDirection.get() === 'right' ? 1 : 0.24,
+ };
+ });
+
return (
@@ -137,6 +149,12 @@ function DeckPhaseFeedback() {
Undo
+
+ Left
+
+
+ Right
+
);
}
@@ -414,6 +432,12 @@ const styles = StyleSheet.create({
undoPhasePill: {
backgroundColor: 'rgba(251, 191, 36, 0.82)',
},
+ leftDismissPill: {
+ backgroundColor: 'rgba(248, 113, 113, 0.82)',
+ },
+ rightDismissPill: {
+ backgroundColor: 'rgba(74, 222, 128, 0.82)',
+ },
phaseText: {
color: '#09090b',
fontSize: 10,
diff --git a/src/__tests__/SwipeDeck.integration.test.tsx b/src/__tests__/SwipeDeck.integration.test.tsx
index f594c1e..0d61519 100644
--- a/src/__tests__/SwipeDeck.integration.test.tsx
+++ b/src/__tests__/SwipeDeck.integration.test.tsx
@@ -41,6 +41,7 @@ describe('SwipeDeck factory hooks', () => {
function DeckControls() {
const state = ProfileDeck.useDeckState();
const actions = ProfileDeck.useDeckActions();
+ const interaction = ProfileDeck.useDeckInteraction();
const [lastActionResult, setLastActionResult] = useState('none');
return (
@@ -53,7 +54,13 @@ describe('SwipeDeck factory hooks', () => {
setLastActionResult(String(actions.swipeRight()))}
+ onPress={() => {
+ const accepted = actions.swipeRight();
+
+ setLastActionResult(
+ `${String(accepted)}:${interaction.dismissDirection.get() ?? 'null'}`,
+ );
+ }}
>
Force swipe right
@@ -94,7 +101,7 @@ describe('SwipeDeck factory hooks', () => {
await user.press(screen.getByRole('button', { name: 'Force swipe right' }));
- expect(screen.getByText('action:false')).toBeOnTheScreen();
+ expect(screen.getByText('action:false:null')).toBeOnTheScreen();
await measureDeckFromVisibleCard('Ada');
@@ -103,7 +110,7 @@ describe('SwipeDeck factory hooks', () => {
await user.press(screen.getByRole('button', { name: 'Force swipe right' }));
- expect(await screen.findByText('action:true')).toBeOnTheScreen();
+ expect(await screen.findByText('action:true:right')).toBeOnTheScreen();
expect(await screen.findByText('state:1:2:true:false')).toBeOnTheScreen();
expect(screen.getByText('Grace')).toBeOnTheScreen();
expect(onSwipe).toHaveBeenCalledWith({
@@ -256,11 +263,11 @@ describe('SwipeDeck factory hooks', () => {
const actions = ProfileDeck.useDeckActions();
const interaction = ProfileDeck.useDeckInteraction();
const [lastActionResult, setLastActionResult] = useState('none');
- const [interactionText, setInteractionText] = useState('0:0:0:idle');
+ const [interactionText, setInteractionText] = useState('0:0:0:null:idle');
useEffect(() => {
setInteractionText(
- `${interaction.progress.get()}:${interaction.signedProgress.get()}:${interaction.direction.get()}:${interaction.phase.get()}`,
+ `${interaction.progress.get()}:${interaction.signedProgress.get()}:${interaction.direction.get()}:${interaction.dismissDirection.get() ?? 'null'}:${interaction.phase.get()}`,
);
}, [interaction, state.activeIndex, state.canSwipe, state.isCompleted]);
@@ -306,13 +313,13 @@ describe('SwipeDeck factory hooks', () => {
await measureDeckFromVisibleCard('Ada');
expect(await screen.findByText('state:0:2:true:false')).toBeOnTheScreen();
- expect(screen.getByText('interaction:0:0:0:idle')).toBeOnTheScreen();
+ expect(screen.getByText('interaction:0:0:0:null:idle')).toBeOnTheScreen();
await user.press(screen.getByRole('button', { name: 'Force swipe right' }));
expect(await screen.findByText('action:true')).toBeOnTheScreen();
expect(await screen.findByText('state:1:2:true:false')).toBeOnTheScreen();
- expect(screen.getByText('interaction:0:0:0:idle')).toBeOnTheScreen();
+ expect(screen.getByText('interaction:0:0:0:null:idle')).toBeOnTheScreen();
expect(screen.getByText('Grace')).toBeOnTheScreen();
expect(onSwipe).toHaveBeenCalledTimes(1);
expect(onSwipe).toHaveBeenCalledWith({
@@ -493,7 +500,7 @@ describe('SwipeDeck factory hooks', () => {
const accepted = actions.swipeRight();
setProbe(
- `${String(accepted)}:${interaction.direction.get()}:${interaction.phase.get()}`,
+ `${String(accepted)}:${interaction.direction.get()}:${interaction.dismissDirection.get() ?? 'null'}:${interaction.phase.get()}`,
);
}}
>
@@ -531,7 +538,7 @@ describe('SwipeDeck factory hooks', () => {
await user.press(screen.getByRole('button', { name: 'Probe swipe right' }));
- expect(await screen.findByText('probe:true:1:dismissing')).toBeOnTheScreen();
+ expect(await screen.findByText('probe:true:1:right:dismissing')).toBeOnTheScreen();
expect(screen.getByText('Grace')).toBeOnTheScreen();
await renderResult.rerender(
@@ -540,7 +547,7 @@ describe('SwipeDeck factory hooks', () => {
await user.press(screen.getByRole('button', { name: 'Probe swipe right' }));
- expect(await screen.findByText('probe:true:0:dismissing')).toBeOnTheScreen();
+ expect(await screen.findByText('probe:true:0:right:dismissing')).toBeOnTheScreen();
expect(screen.getByText('Linus')).toBeOnTheScreen();
});
@@ -560,11 +567,11 @@ describe('SwipeDeck factory hooks', () => {
const actions = ProfileDeck.useDeckActions();
const interaction = ProfileDeck.useDeckInteraction();
const [lastActionResult, setLastActionResult] = useState('none');
- const [interactionText, setInteractionText] = useState('0:0:0:idle');
+ const [interactionText, setInteractionText] = useState('0:0:0:null:idle');
useEffect(() => {
setInteractionText(
- `${interaction.progress.get()}:${interaction.signedProgress.get()}:${interaction.direction.get()}:${interaction.phase.get()}`,
+ `${interaction.progress.get()}:${interaction.signedProgress.get()}:${interaction.direction.get()}:${interaction.dismissDirection.get() ?? 'null'}:${interaction.phase.get()}`,
);
}, [interaction, state.activeIndex, state.canUndo]);
@@ -648,7 +655,7 @@ describe('SwipeDeck factory hooks', () => {
expect(await screen.findByText('state:0:true:false:false')).toBeOnTheScreen();
expect(screen.getByText('Ada')).toBeOnTheScreen();
- expect(screen.getByText('interaction:0:0:0:idle')).toBeOnTheScreen();
+ expect(screen.getByText('interaction:0:0:0:null:idle')).toBeOnTheScreen();
expect(onSwipe).toHaveBeenCalledTimes(1);
expect(onUndo).toHaveBeenCalledWith({
direction: 'right',
@@ -669,10 +676,16 @@ describe('SwipeDeck factory hooks', () => {
it('tracks undo history from committed pan gestures when undo is enabled', async () => {
const ProfileDeck = createSwipeDeck();
const user = userEvent.setup();
+ const latestInteraction: {
+ current: ReturnType | null;
+ } = { current: null };
function DeckControls() {
const state = ProfileDeck.useDeckState();
const actions = ProfileDeck.useDeckActions();
+ const interaction = ProfileDeck.useDeckInteraction();
+
+ latestInteraction.current = interaction;
return (
@@ -707,6 +720,7 @@ describe('SwipeDeck factory hooks', () => {
{ state: 5, translationX: 180, translationY: 0, velocityX: 0, y: 250 },
]);
+ expect(latestInteraction.current?.dismissDirection.get()).toBe('right');
expect(await screen.findByText('state:1:true:true:false')).toBeOnTheScreen();
expect(screen.getByText('Grace')).toBeOnTheScreen();
@@ -750,7 +764,9 @@ describe('SwipeDeck factory hooks', () => {
}),
);
- setProbe(`${String(accepted)}:${interaction.phase.get()}`);
+ setProbe(
+ `${String(accepted)}:${interaction.dismissDirection.get() ?? 'null'}:${interaction.phase.get()}`,
+ );
}}
>
Probe undo
@@ -779,7 +795,7 @@ describe('SwipeDeck factory hooks', () => {
await user.press(screen.getByRole('button', { name: 'Probe undo' }));
- expect(await screen.findByText('probe:true:undoing')).toBeOnTheScreen();
+ expect(await screen.findByText('probe:true:null:undoing')).toBeOnTheScreen();
expect(await screen.findByText('state:0:true:false:false')).toBeOnTheScreen();
});
@@ -790,11 +806,11 @@ describe('SwipeDeck factory hooks', () => {
function DeckControls() {
const state = ProfileDeck.useDeckState();
const interaction = ProfileDeck.useDeckInteraction();
- const [interactionText, setInteractionText] = useState('0:0:0:idle');
+ const [interactionText, setInteractionText] = useState('0:0:0:null:idle');
useEffect(() => {
setInteractionText(
- `${interaction.progress.get()}:${interaction.signedProgress.get()}:${interaction.direction.get()}:${interaction.phase.get()}`,
+ `${interaction.progress.get()}:${interaction.signedProgress.get()}:${interaction.direction.get()}:${interaction.dismissDirection.get() ?? 'null'}:${interaction.phase.get()}`,
);
}, [interaction, state.activeIndex, state.canSwipe, state.canUndo]);
@@ -837,7 +853,7 @@ describe('SwipeDeck factory hooks', () => {
]);
expect(await screen.findByText('state:0:true:false:false')).toBeOnTheScreen();
- expect(screen.getByText('interaction:0:0:0:idle')).toBeOnTheScreen();
+ expect(screen.getByText('interaction:0:0:0:null:idle')).toBeOnTheScreen();
expect(screen.getByText('Ada')).toBeOnTheScreen();
expect(onSwipe).not.toHaveBeenCalled();
});
diff --git a/src/__tests__/registry.test.ts b/src/__tests__/registry.test.ts
index 44b72ca..a3cb26c 100644
--- a/src/__tests__/registry.test.ts
+++ b/src/__tests__/registry.test.ts
@@ -53,6 +53,7 @@ describe('createSwipeDeckRegistry', () => {
expect(registry.getStore('profiles').actions).toBe(store.actions);
expect(registry.getStore('profiles').interaction).toBe(store.interaction);
expect(store.interaction.phase.get()).toBe('idle');
+ expect(store.interaction.dismissDirection.get()).toBeNull();
});
it('notifies state subscribers only when the snapshot changes', () => {
@@ -270,6 +271,7 @@ describe('createSwipeDeckRegistry', () => {
store.interaction.progress.set(1);
store.interaction.signedProgress.set(-1);
store.interaction.direction.set(-1);
+ store.interaction.dismissDirection.set('left');
store.interaction.translationX.set(-120);
store.interaction.translationY.set(24);
store.interaction.isDragging.set(true);
@@ -280,6 +282,7 @@ describe('createSwipeDeckRegistry', () => {
expect(store.interaction.progress.get()).toBe(0);
expect(store.interaction.signedProgress.get()).toBe(0);
expect(store.interaction.direction.get()).toBe(0);
+ expect(store.interaction.dismissDirection.get()).toBeNull();
expect(store.interaction.translationX.get()).toBe(0);
expect(store.interaction.translationY.get()).toBe(0);
expect(store.interaction.isDragging.get()).toBe(false);
diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx
index 60d3cf6..a7aacf8 100644
--- a/src/components/SwipeDeck.tsx
+++ b/src/components/SwipeDeck.tsx
@@ -87,6 +87,7 @@ function Root({
const swipeProgress = interaction.progress;
const signedSwipeProgress = interaction.signedProgress;
const swipeDirectionSignal = interaction.direction;
+ const dismissDirection = interaction.dismissDirection;
const activeTranslateX = interaction.translationX;
const activeTranslateY = interaction.translationY;
const isDragging = interaction.isDragging;
@@ -231,6 +232,7 @@ function Root({
data,
disabledRef,
dismissRuntimeRef,
+ dismissDirection,
dragItemIndex,
endReachedRef,
gestureStartYRatio,
@@ -268,6 +270,7 @@ function Root({
dataRef,
disabledRef,
dismissRuntimeRef,
+ dismissDirection,
dragItemIndex,
endReachedRef,
gestureStartYRatio,
@@ -299,6 +302,7 @@ function Root({
dismissMaxDuration,
dismissMinDuration,
dismissOffscreenMultiplier,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
hasActiveCard,
diff --git a/src/core/swipeDeckRuntime.ts b/src/core/swipeDeckRuntime.ts
index ee1ccde..69317d9 100644
--- a/src/core/swipeDeckRuntime.ts
+++ b/src/core/swipeDeckRuntime.ts
@@ -1,3 +1,5 @@
+import type { SharedValue } from 'react-native-reanimated';
+
import type {
SwipeDeckActionMotionRecipe,
SwipeDeckLayout,
@@ -42,6 +44,13 @@ type ResolveSwipeDeckProgrammaticUndoMotionArgs = {
undoMotion?: SwipeDeckUndoMotionRecipe;
};
+type ResetSwipeDeckInteractionSignalsArgs = {
+ dismissDirection: SharedValue;
+ signedSwipeProgress: SharedValue;
+ swipeDirectionSignal: SharedValue<-1 | 0 | 1>;
+ swipeProgress: SharedValue;
+};
+
export function resolveProgressDirection(translationX: number): -1 | 0 | 1 {
'worklet';
@@ -64,6 +73,20 @@ export function resolveSignedSwipeProgress(translationX: number, distance: numbe
return direction * Math.min(Math.abs(translationX) / Math.max(distance, 1), 1);
}
+export function resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+}: ResetSwipeDeckInteractionSignalsArgs) {
+ 'worklet';
+
+ swipeProgress.set(0);
+ signedSwipeProgress.set(0);
+ swipeDirectionSignal.set(0);
+ dismissDirection.set(null);
+}
+
export function getActiveRenderItemId(dataLength: number, activeIndex: number): number {
if (activeIndex < 0 || activeIndex >= dataLength) {
return -1;
diff --git a/src/hooks/useSwipeDeckDismissRuntime.ts b/src/hooks/useSwipeDeckDismissRuntime.ts
index 9ca8f59..91ff460 100644
--- a/src/hooks/useSwipeDeckDismissRuntime.ts
+++ b/src/hooks/useSwipeDeckDismissRuntime.ts
@@ -16,7 +16,10 @@ import type {
} from '../types';
import { getSwipeCommit, shouldDeferActiveItemSync } from '../core/state';
-import { resolveSwipeDeckProgrammaticActionMotion } from '../core/swipeDeckRuntime';
+import {
+ resetSwipeDeckInteractionSignals,
+ resolveSwipeDeckProgrammaticActionMotion,
+} from '../core/swipeDeckRuntime';
import {
resolveSwipeDeckDismissDestinationDistance,
resolveSwipeDeckDismissDuration,
@@ -55,6 +58,7 @@ type UseSwipeDeckDismissRuntimeArgs = {
dataRef: RefObject;
disabledRef: RefObject;
dismissRuntimeRef: RefObject;
+ dismissDirection: SharedValue;
dragItemIndex: SharedValue;
endReachedRef: RefObject;
emitDeckEvent: >(
@@ -111,6 +115,7 @@ export function useSwipeDeckDismissRuntime({
dataRef,
disabledRef,
dismissRuntimeRef,
+ dismissDirection,
dragItemIndex,
endReachedRef,
emitDeckEvent,
@@ -182,9 +187,12 @@ export function useSwipeDeckDismissRuntime({
cancelActiveInteractionAnimations();
activeTranslateX.set(0);
activeTranslateY.set(0);
- swipeProgress.set(0);
- signedSwipeProgress.set(0);
- swipeDirectionSignal.set(0);
+ resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+ });
isDragging.set(false);
interactionPhase.set('idle');
gestureStartYRatio.set(0.5);
@@ -193,6 +201,7 @@ export function useSwipeDeckDismissRuntime({
activeTranslateX,
activeTranslateY,
cancelActiveInteractionAnimations,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
isDragging,
@@ -273,6 +282,7 @@ export function useSwipeDeckDismissRuntime({
isAnimating.set(true);
isDragging.set(true);
interactionPhase.set('dismissing');
+ dismissDirection.set(direction);
applyImmediateRuntimeState(true, true);
gestureStartYRatio.set(0.5);
dragItemIndex.set(activeItemIndex.get());
@@ -367,6 +377,7 @@ export function useSwipeDeckDismissRuntime({
dataRef,
disabledRef,
dismissRuntimeRef,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
isAnimating,
diff --git a/src/hooks/useSwipeDeckGestureRuntime.ts b/src/hooks/useSwipeDeckGestureRuntime.ts
index 5a6a73d..813ca13 100644
--- a/src/hooks/useSwipeDeckGestureRuntime.ts
+++ b/src/hooks/useSwipeDeckGestureRuntime.ts
@@ -48,6 +48,7 @@ type UseSwipeDeckGestureRuntimeArgs = {
dismissMaxDuration: number;
dismissMinDuration: number;
dismissOffscreenMultiplier: number;
+ dismissDirection: SharedValue;
dragItemIndex: SharedValue;
gestureStartYRatio: SharedValue;
hasActiveCard: boolean;
@@ -85,6 +86,7 @@ export function useSwipeDeckGestureRuntime({
dismissMaxDuration,
dismissMinDuration,
dismissOffscreenMultiplier,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
hasActiveCard,
@@ -122,6 +124,7 @@ export function useSwipeDeckGestureRuntime({
runtimeEventId.set(nextRuntimeEventId);
isDragging.set(true);
interactionPhase.set('dragging');
+ dismissDirection.set(null);
swipeDirectionSignal.set(0);
signedSwipeProgress.set(0);
scheduleOnRN(applyScheduledRuntimeState, nextRuntimeEventId, false, true);
@@ -187,6 +190,7 @@ export function useSwipeDeckGestureRuntime({
isAnimating.set(false);
isDragging.set(false);
interactionPhase.set('idle');
+ dismissDirection.set(null);
swipeDirectionSignal.set(0);
const nextRuntimeEventId = runtimeEventId.get() + 1;
@@ -205,6 +209,7 @@ export function useSwipeDeckGestureRuntime({
isAnimating.set(true);
isDragging.set(true);
interactionPhase.set('dismissing');
+ dismissDirection.set(direction);
const currentAttachmentGeneration = attachmentGeneration.get();
scheduleOnRN(applyScheduledRuntimeState, runtimeEventId.get(), true, true);
@@ -285,6 +290,7 @@ export function useSwipeDeckGestureRuntime({
dismissMaxDuration,
dismissMinDuration,
dismissOffscreenMultiplier,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
hasActiveCard,
diff --git a/src/hooks/useSwipeDeckUndoRuntime.ts b/src/hooks/useSwipeDeckUndoRuntime.ts
index 9d5aad5..0a3760d 100644
--- a/src/hooks/useSwipeDeckUndoRuntime.ts
+++ b/src/hooks/useSwipeDeckUndoRuntime.ts
@@ -17,6 +17,7 @@ import type {
import {
getActiveRenderItemId,
+ resetSwipeDeckInteractionSignals,
resolveSwipeDeckProgrammaticUndoMotion,
} from '../core/swipeDeckRuntime';
import { type ResolvedSwipeDeckUndoMotion } from '../motion/undoMotion';
@@ -77,6 +78,7 @@ type UseSwipeDeckUndoRuntimeArgs = {
data: readonly T[];
disabledRef: RefObject;
dismissRuntimeRef: RefObject;
+ dismissDirection: SharedValue;
dragItemIndex: SharedValue;
endReachedRef: RefObject;
emitDeckEvent: >(
@@ -126,6 +128,7 @@ export function useSwipeDeckUndoRuntime({
data,
disabledRef,
dismissRuntimeRef,
+ dismissDirection,
dragItemIndex,
endReachedRef,
emitDeckEvent,
@@ -185,9 +188,12 @@ export function useSwipeDeckUndoRuntime({
setUndoTransition(null);
cancelActiveInteractionAnimations();
cancelAnimation(undoProgress);
- swipeProgress.set(0);
- signedSwipeProgress.set(0);
- swipeDirectionSignal.set(0);
+ resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+ });
activeTranslateX.set(0);
activeTranslateY.set(0);
activeItemIndex.set(getActiveRenderItemId(dataRef.current.length, activeIndexRef.current));
@@ -206,6 +212,7 @@ export function useSwipeDeckUndoRuntime({
activeTranslateY,
applyImmediateRuntimeState,
cancelActiveInteractionAnimations,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
isAnimating,
@@ -289,9 +296,12 @@ export function useSwipeDeckUndoRuntime({
activeItemIndex.set(getActiveRenderItemId(currentData.length, activeIndexRef.current));
activeTranslateX.set(0);
activeTranslateY.set(0);
- swipeProgress.set(0);
- signedSwipeProgress.set(0);
- swipeDirectionSignal.set(0);
+ resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+ });
dragItemIndex.set(-1);
undoProgress.set(0);
undoFromTranslateX.set(0);
@@ -322,9 +332,12 @@ export function useSwipeDeckUndoRuntime({
setEndReached(false);
activeIndexRef.current = restoredIndex;
activeItemIndex.set(restoredIndex);
- swipeProgress.set(0);
- signedSwipeProgress.set(0);
- swipeDirectionSignal.set(0);
+ resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+ });
activeTranslateX.set(0);
activeTranslateY.set(0);
dragItemIndex.set(-1);
@@ -352,6 +365,7 @@ export function useSwipeDeckUndoRuntime({
applyImmediateRuntimeState,
attachmentGenerationRef,
cancelPendingUndoRestore,
+ dismissDirection,
dragItemIndex,
endReachedRef,
gestureStartYRatio,
@@ -422,9 +436,12 @@ export function useSwipeDeckUndoRuntime({
isAnimating.set(true);
interactionPhase.set('undoing');
applyImmediateRuntimeState(true, false);
- swipeProgress.set(0);
- signedSwipeProgress.set(0);
- swipeDirectionSignal.set(0);
+ resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+ });
activeTranslateX.set(0);
activeTranslateY.set(0);
isDragging.set(false);
@@ -448,6 +465,7 @@ export function useSwipeDeckUndoRuntime({
cancelActiveInteractionAnimations,
disabledRef,
dismissRuntimeRef,
+ dismissDirection,
dragItemIndex,
gestureStartYRatio,
isAnimating,
@@ -483,9 +501,12 @@ export function useSwipeDeckUndoRuntime({
cancelAnimation(undoProgress);
dragItemIndex.set(-1);
gestureStartYRatio.set(0.5);
- swipeProgress.set(0);
- signedSwipeProgress.set(0);
- swipeDirectionSignal.set(0);
+ resetSwipeDeckInteractionSignals({
+ dismissDirection,
+ signedSwipeProgress,
+ swipeDirectionSignal,
+ swipeProgress,
+ });
activeTranslateX.set(0);
activeTranslateY.set(0);
undoProgress.set(1);
@@ -520,6 +541,7 @@ export function useSwipeDeckUndoRuntime({
cancelActiveInteractionAnimations,
completeUndoRestoreIfCurrent,
dragItemIndex,
+ dismissDirection,
gestureStartYRatio,
signedSwipeProgress,
swipeDirectionSignal,
diff --git a/src/registry/registry.ts b/src/registry/registry.ts
index 7a52851..a21c379 100644
--- a/src/registry/registry.ts
+++ b/src/registry/registry.ts
@@ -70,6 +70,7 @@ function createInteraction(): SwipeDeckInteraction {
progress: makeMutable(0),
signedProgress: makeMutable(0),
direction: makeMutable<-1 | 0 | 1>(0),
+ dismissDirection: makeMutable(null),
translationX: makeMutable(0),
translationY: makeMutable(0),
isDragging: makeMutable(false),
@@ -81,6 +82,7 @@ function resetInteraction(interaction: SwipeDeckInteraction) {
interaction.progress.set(0);
interaction.signedProgress.set(0);
interaction.direction.set(0);
+ interaction.dismissDirection.set(null);
interaction.translationX.set(0);
interaction.translationY.set(0);
interaction.isDragging.set(false);
diff --git a/src/types.ts b/src/types.ts
index bd75e99..a1b7194 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -534,6 +534,13 @@ export type SwipeDeckInteraction = {
signedProgress: SharedValue;
/** Current swipe direction signal; left is `-1`, idle is `0`, right is `1`. */
direction: SharedValue<-1 | 0 | 1>;
+ /**
+ * Accepted dismiss direction for lifecycle-driven visual feedback.
+ *
+ * This is not the committed swipe event payload. It becomes non-null only after a dismiss is
+ * accepted and resets after the deck commits and clears interaction values.
+ */
+ dismissDirection: SharedValue;
/** Active card horizontal translation. */
translationX: SharedValue;
/** Active card vertical translation. */