'use client';
import { useState } from 'react';
import { useDrag } from '@use-gesture/react';
import { useSpring, animated } from '@react-spring/web';
import { Trash2, Archive, Check } from 'lucide-react';
/**
* SwipeableRow — Telegram-style swipeable list item.
*
* Swipe left to reveal action buttons (delete, archive, etc.)
* Works on both iOS and Android with physics-based spring animation.
*
* Usage:
* removeItem(id)}>
*
Item content here
*
*
* Props:
* - onDelete: called when the delete action is triggered
* - onArchive: optional, called when archive action is triggered
* - threshold: drag distance to trigger action (default: 120px)
*/
export function SwipeableRow({
children,
onDelete,
onArchive,
threshold = 120,
}: {
children: React.ReactNode;
onDelete?: () => void;
onArchive?: () => void;
threshold?: number;
}) {
const [{ x }, api] = useSpring(() => ({ x: 0 }));
const [showActions, setShowActions] = useState(false);
const bind = useDrag(
({ down, movement: [mx], direction: [dx], velocity: [vx] }) => {
// Only allow leftward drag (negative x) to reveal actions
const clamped = Math.max(0, -mx); // 0 to threshold
const maxDrag = onArchive ? 160 : 80; // wider if 2 actions
if (down) {
// While dragging: follow finger with slight resistance
api.start({ x: Math.max(-maxDrag, -clamped), immediate: true });
} else {
// Released: snap to open or closed based on threshold
if (clamped > threshold || (vx > 0.5 && dx < 0)) {
// Snap open to reveal actions
api.start({ x: -maxDrag });
setShowActions(true);
} else {
// Snap closed
api.start({ x: 0 });
setShowActions(false);
}
}
},
{ axis: 'x', filterTaps: true }
);
const handleClose = () => {
api.start({ x: 0 });
setShowActions(false);
};
const handleDelete = () => {
api.start({ x: -500, immediate: true });
setTimeout(() => {
onDelete?.();
handleClose();
}, 200);
};
const handleArchive = () => {
onArchive?.();
handleClose();
};
return (
{/* Action buttons behind the row */}
{onArchive && (
)}
{onDelete && (
)}
{/* The draggable row content */}
{children}
);
}
/**
* SwipeableCard — a simpler variant for cards that can be swiped away.
* When swiped past the threshold, the card animates off-screen and
* calls onDismiss.
*/
export function SwipeableCard({
children,
onDismiss,
}: {
children: React.ReactNode;
onDismiss?: () => void;
}) {
const [{ x, opacity }, api] = useSpring(() => ({ x: 0, opacity: 1 }));
const bind = useDrag(
({ down, movement: [mx], direction: [dx], velocity: [vx] }) => {
if (down) {
api.start({ x: mx, opacity: 1 - Math.min(Math.abs(mx) / 300, 0.5), immediate: true });
} else {
// If dragged far enough or flicked, dismiss
if (Math.abs(mx) > 150 || (vx > 0.5 && Math.abs(mx) > 50)) {
const direction = mx > 0 ? 1 : -1;
api.start({ x: direction * 500, opacity: 0 });
setTimeout(() => onDismiss?.(), 300);
} else {
// Snap back
api.start({ x: 0, opacity: 1 });
}
}
},
{ axis: 'x' }
);
return (
{children}
);
}