"use client";
import useHaptic from "@/lib/hooks/useHaptics";
import { cn } from "@/lib/utils";
import { useResizeObserver } from "@mantine/hooks";
import {
motion,
useMotionValue,
animate,
useTransform,
MotionConfig,
AnimatePresence,
useMotionValueEvent,
useSpring,
Transition,
ValueAnimationTransition,
SpringOptions,
} from "framer-motion";
import React, {
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from "react";

interface SlideToConfirmProps extends PropsWithChildren {
onSlideComplete?: () => void;
onLongPress?: () => void;
holdTreshold?: number;
}
export function SlideToConfirm(props: SlideToConfirmProps) {
const { onSlideComplete, onLongPress, holdTreshold = 500 } = props;

const { triggerHapticFeedback, isHapticSupported } = useHaptic();

const [isSlideComplete, setIsSlideComplete] = useState(false);
const [isLongPressed, setIsLongPressed] = useState(false);

const clientX = useMotionValue(0);

const thumbWrapperWidth = useMotionValue(0);
const thumbX = useMotionValue(0);

const [refThumbWrapper, thumbWrapperRect] = useResizeObserver();
const [refThumb, thumbRect] = useResizeObserver();
const [refTrack, trackRect] = useResizeObserver();

const [isDragging, setIsDragging] = useState(false);

const startRef = useRef({ x: 0, width: thumbWrapperRect.height });
const [thumbXMax, setThumbXMax] = useState(0);

const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
const longPressThreshold = holdTreshold;

const transition: Transition = !isSlideComplete
? {
type: "spring",
stiffness: 100,
damping: 7,
mass: 0.2,
}
: {
type: "tween",
duration: 0.27,
ease: "easeOut",
};

const valueTransition: ValueAnimationTransition = { ...transition };

useEffect(() => {
setThumbXMax(
trackRect.width - thumbRect.width - refThumb.current?.offsetLeft * 2,
);
startRef.current.width = thumbWrapperRect.height;
}, [trackRect, thumbRect, refThumb, thumbWrapperRect.height]);

const handleConfetti = useCallback(async () => {
const { clientWidth, clientHeight } = document.documentElement;
const boundingBox = refThumb.current?.getBoundingClientRect?.();


const targetY = boundingBox?.y ?? 0;
const targetX = boundingBox?.x ?? 0;
const targetWidth = boundingBox?.width ?? 0;

const targetCenterX = targetX + targetWidth / 2;

function randomInRange(min: number, max: number) {
  return Math.random() * (max - min) + min;
}

const confetti = (await import("canvas-confetti")).default;

confetti({
  angle: randomInRange(120, 120),
  spread: randomInRange(50, 70),
  particleCount: randomInRange(50, 100),
  zIndex: 999,
  ticks: 60,
  origin: {
    // y: targetY / clientHeight,
    x: targetCenterX / clientWidth,
  },
});
}, [refThumb]);

const startLongPressTimer = useCallback(() => {
if (longPressTimerRef.current) clearTimeout(longPressTimerRef.current);
longPressTimerRef.current = setTimeout(() => {
setIsLongPressed(true);
onLongPress?.();
triggerHapticFeedback(200);
handleConfetti();
}, longPressThreshold);
}, [longPressThreshold, onLongPress, triggerHapticFeedback, handleConfetti]);

const clearLongPressTimer = useCallback(() => {
if (longPressTimerRef.current) {
setIsLongPressed(false);
clearTimeout(longPressTimerRef.current);
longPressTimerRef.current = null;
}
}, []);

const checkSlideComplete = useCallback(() => {
return thumbX.get() >= thumbXMax;
}, [thumbX, thumbXMax]);

const OnSlideComplete = () => {
if (checkSlideComplete()) {
setIsSlideComplete(true);
if (!isSlideComplete) {
onSlideComplete?.();
triggerHapticFeedback(100);
startLongPressTimer();
}
} else {
setIsSlideComplete(false);
clearLongPressTimer();
}
};

useMotionValueEvent(clientX, "change", (latest: number) => {
const dx =
latest -
startRef.current.x -
trackRect.left +
refThumb.current.offsetLeft * 2;
let newWidth = Math.max(
startRef.current.width,
startRef.current.width + dx,
);
// newWidth = clamp(startRef.current.width, trackRect.width, newWidth);
thumbWrapperWidth.jump(newWidth);


let newX = latest - startRef.current.x - trackRect.left + thumbRect.left;
// newX = clamp(0, thumbXMax, newX);
thumbX.jump(newX);
});

const thumbXClamped = useTransform(thumbX, [0, thumbXMax], [0, thumbXMax]);
const thumbXSpring = useSpring(thumbXClamped, {
...transition,
} as SpringOptions);

const thumbWrapperWidthClamped = useTransform(
thumbWrapperWidth,
[startRef.current.width, trackRect.width],
[startRef.current.width, trackRect.width],
);
const thumbWrapperWidthSpring = useSpring(thumbWrapperWidthClamped, {
...transition,
} as SpringOptions);

const handlePointerDown = (event: React.PointerEvent) => {
startRef.current = {
x: event.clientX,
width: thumbWrapperRect.height,
};
setIsDragging(true);
};

const handlePointerMove = (event: React.PointerEvent) => {
event.preventDefault();
if (!isDragging) return;
(event.target as HTMLDivElement).setPointerCapture(event.pointerId);
clientX.jump(event.clientX);
OnSlideComplete();
};

const handlePointerUp = () => {
setIsDragging(false);
animate(clientX, 0, {
...valueTransition,
});
clearLongPressTimer();
setIsSlideComplete(false);
};

const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

const [cannon, setCannon] = useState(false);
useEffect(() => {
if (cannon == true) {
setCannon(false);
}
}, [cannon]);

return (
<MotionConfig transition={{ ...transition }}>
<motion.div
initial={{
opacity: 0,
scale: 0.8,
}}
animate={{
opacity: refThumb.current ? 1 : 0,
scale: 1,
}}
ref={refTrack}
className={cn(
"max-w-72 overflow-hidden min-w-48 touch-none relative border w-full h-16 bg-neutral-50/10 rounded-3xl border-neutral-50/10 flex justify-between items-center",
)}
>

{isSlideComplete && (
<motion.div
layout={"size"}
initial={{
width: 0,
borderRadius: "24px",
originX: 0,
}}
animate={{
originX: 0,
borderRadius: "24px",
width: trackRect.width,
}}
exit={{
originX: 0,
borderRadius: "24px",
width: 0,
opacity: 0,
transition: {
width: {
duration: 0.27,
ease: "easeOut",
},
},
}}
transition={{
width: {
duration: holdTreshold / 1000,
ease: "easeOut",
},
}}
className="absolute left-0 inset-y-0 blur-md bg-gradient-to-r from-transparent to-emerald-500/50"
/>
)}

<motion.div
style={{
width: refThumb.current
? calc(100% - ${startRef.current.width}px)
: "100%",
x:
thumbX.get() < thumbXMax / 2
? ${startRef.current.width}px
: "0",
}}
className={cn(
"flex-1 pointer-events-none absolute duration-300 select-none h-full flex justify-center items-center",
)}
>



{isLongPressed ? (
<motion.div
key="release"
initial={{
opacity: 0,
y: 20,
}}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.1,
},
}}
exit={{
opacity: 0,
y: -20,
transition: {
duration: 0.1,
},
}}
className="text-white w-full text-nowrap font-medium select-none"
>
Release
</motion.div>
) : (
<motion.div
key="slide"
initial={{
opacity: 0,
y: 20,
}}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.1,
},
}}
exit={{
opacity: 0,
y: -20,
transition: {
duration: 0.1,
},
}}
className="text-white w-full text-nowrap font-medium select-none"
>
Slide & Hold
</motion.div>
)}


</motion.div>
<motion.div
ref={refThumbWrapper}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onLostPointerCapture={handlePointerUp}
onPointerCancel={handlePointerUp}
onPointerUp={handlePointerUp}
style={{
width: useTransform(() => thumbWrapperWidthSpring.get()),
}}
className={cn(
"min-w-16 z-10 rounded-[23px] cursor-grab active:cursor-grabbing relative bg-gradient-to-r from-transparent to-neutral-50/20 h-full flex justify-start items-center p-2 aspect-square",
)}
>
<motion.div
ref={refThumb}
style={{
x: useTransform(() => thumbXSpring.get()),
}}
className={cn(
"flex justify-center items-center h-full aspect-square rounded-2xl transition-colors",
isSlideComplete ? "bg-emerald-500" : "bg-neutral-100",
)}
>

<div
className={cn(
"h-4 w-1 animate-[pulse_1.3s_ease_0ms_infinite] delay-0 rounded-2xl border-t-[1px] border-l-[1px] transition-colors",
isSlideComplete
? "bg-emerald-200 border-emerald-400/40"
: "bg-neutral-400/60 border-neutral-400/40",
)}
/>
<div
className={cn(
"h-3 w-1 animate-[pulse_1.3s_ease_250ms_infinite] delay-150 rounded-2xl border-t-[1px] border-l-[1px] transition-colors",
isSlideComplete
? "bg-emerald-200 border-emerald-400/40"
: "bg-neutral-400/60 border-neutral-400/40",
)}
/>
<div
className={cn(
"h-1.5 w-1 animate-[pulse_1.3s_ease_500ms_infinite] delay-300 rounded-2xl border-t-[1px] border-l-[1px] transition-colors",
isSlideComplete
? "bg-emerald-200 border-emerald-400/40"
: "bg-neutral-400/60 border-neutral-400/40",
)}
/>

</motion.div>
</motion.div>
</motion.div>
{isClient && (
<motion.div className="text-xs mt-8 font-medium text-neutral-500">
Haptics:{" "}
<motion.span>
{isHapticSupported ? "Supported" : "Unsupported"}
</motion.span>
</motion.div>
)}
{/* <motion.div className="text-xs font-medium text-neutral-500">
isLongPressed: <motion.span>{isLongPressed.toString()}</motion.span>
</motion.div>
<motion.div className="text-xs font-medium text-neutral-500">
isSlideComplete: <motion.span>{isSlideComplete.toString()}</motion.span>
</motion.div> */}

);
}
this is the code I explored and made.

Write a "X" post for showing exploration and progress.