"use client"; // Adapted from Kokonut UI (@dorianbaffier), MIT. See THIRD_PARTY_NOTICES.md. import { useEffect, useId, useRef, useState, type ComponentProps, type CSSProperties, } from "react"; export interface HoldButtonProps extends Omit< ComponentProps<"button">, | "onClick" | "onPointerDown" | "onPointerUp" | "onPointerCancel" | "onPointerLeave" | "onKeyDown" | "onKeyUp" | "onBlur" | "type" > { onConfirm: () => void; holdDuration?: number; /** Retained for source compatibility; short clicks never confirm. */ confirmLabel?: string; instructions?: string; } export function HoldButton({ onConfirm, holdDuration = 1000, confirmLabel: _confirmLabel, instructions = "Hold until the progress completes to confirm. Escape cancels.", children, className = "", disabled, style, ...props }: HoldButtonProps) { const [holding, setHolding] = useState(false); const id = useId(); const timer = useRef | undefined>(undefined); const active = useRef(false); const gesture = useRef<"pointer" | "keyboard" | null>(null); const callback = useRef(onConfirm); callback.current = onConfirm; const duration = Number.isFinite(holdDuration) ? Math.max(200, holdDuration) : 1000; function cancel() { clearTimeout(timer.current); timer.current = undefined; active.current = false; gesture.current = null; setHolding(false); } function start(source: "pointer" | "keyboard") { if (disabled || gesture.current) return; gesture.current = source; active.current = true; setHolding(true); timer.current = setTimeout(() => { active.current = false; setHolding(false); // Keep the gesture latched until release to avoid key-repeat confirmation. callback.current(); }, duration); } useEffect(() => { cancel(); const hidden = () => { if (document.hidden) cancel(); }; document.addEventListener("visibilitychange", hidden); window.addEventListener("blur", cancel); return () => { clearTimeout(timer.current); document.removeEventListener("visibilitychange", hidden); window.removeEventListener("blur", cancel); }; }, [disabled, duration]); return ( <> {instructions} ); }