"use client"; import { useEffect, useRef, useState } from "react"; type ConfirmSlideModalProps = { isOpen: boolean; title: string; description?: string; confirmLabel?: string; onClose: () => void; onConfirm: () => void; }; export default function ConfirmSlideModal({ isOpen, title, description, confirmLabel = "Confirm", onClose, onConfirm }: ConfirmSlideModalProps) { const trackRef = useRef(null); const endFlashTimeoutRef = useRef | null>(null); const reachedEndRef = useRef(false); const [dragX, setDragX] = useState(0); const [dragging, setDragging] = useState(false); const [isAtEnd, setIsAtEnd] = useState(false); const [endFlash, setEndFlash] = useState(false); const handleSize = 40; function getDragPositionFromClientX(clientX: number) { const track = trackRef.current; if (!track) return 0; const rect = track.getBoundingClientRect(); return Math.min(Math.max(0, clientX - rect.left - handleSize / 2), rect.width - handleSize); } function isEndPosition(position: number) { const track = trackRef.current; if (!track) return false; const maxDrag = track.clientWidth - handleSize; const endTolerancePx = 1; return position >= maxDrag - endTolerancePx; } function triggerEndFeedback() { setEndFlash(true); if (endFlashTimeoutRef.current) clearTimeout(endFlashTimeoutRef.current); endFlashTimeoutRef.current = setTimeout(() => setEndFlash(false), 140); if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") { navigator.vibrate(16); } } function handlePointerDown(event: React.PointerEvent) { event.preventDefault(); setDragging(true); reachedEndRef.current = false; setIsAtEnd(false); (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); } function handlePointerMove(event: React.PointerEvent) { if (!dragging) return; const next = getDragPositionFromClientX(event.clientX); const nextAtEnd = isEndPosition(next); setDragX(next); setIsAtEnd(prev => (prev === nextAtEnd ? prev : nextAtEnd)); if (nextAtEnd && !reachedEndRef.current) { reachedEndRef.current = true; triggerEndFeedback(); } if (!nextAtEnd) reachedEndRef.current = false; } function handlePointerUp(event: React.PointerEvent) { if (!dragging) return; setDragging(false); (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId); const track = trackRef.current; if (!track) return; const releaseX = getDragPositionFromClientX(event.clientX); const releaseAtEnd = isEndPosition(releaseX); setIsAtEnd(prev => (prev ? false : prev)); if (releaseAtEnd && !reachedEndRef.current) { triggerEndFeedback(); } if (releaseAtEnd) { setDragX(0); onConfirm(); } else { setDragX(0); } } function handlePointerCancel(event: React.PointerEvent) { if (!dragging) return; setDragging(false); (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId); setIsAtEnd(prev => (prev ? false : prev)); setDragX(0); } useEffect(() => () => { if (endFlashTimeoutRef.current) clearTimeout(endFlashTimeoutRef.current); }, []); useEffect(() => { if (!isOpen) return; function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") onClose(); } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isOpen, onClose]); if (!isOpen) return null; return (
event.stopPropagation()}>
{title}
{description ?

{description}

: null}
Slide to confirm
{confirmLabel}
); }