import React, { useState, useRef, useCallback, useEffect } from "react"; interface Props { content: React.ReactNode; children: React.ReactElement; delayMs?: number; side?: "top" | "bottom"; } export function Tooltip({ content, children, delayMs = 150, side = "top" }: Props) { const [visible, setVisible] = useState(false); const [position, setPosition] = useState<{ x: number; y: number } | null>(null); const triggerRef = useRef(null); const tooltipRef = useRef(null); const timerRef = useRef>(); const show = useCallback(() => { timerRef.current = setTimeout(() => setVisible(true), delayMs); }, [delayMs]); const hide = useCallback(() => { clearTimeout(timerRef.current); setVisible(false); }, []); // Clean up timer on unmount useEffect(() => () => clearTimeout(timerRef.current), []); // Recompute position when visible useEffect(() => { if (!visible || !triggerRef.current) return; const rect = triggerRef.current.getBoundingClientRect(); setPosition({ x: rect.left + rect.width / 2, y: side === "top" ? rect.top : rect.bottom, }); }, [visible, side]); // Nudge tooltip horizontally if it overflows the viewport useEffect(() => { if (!visible || !tooltipRef.current || !position) return; const el = tooltipRef.current; const tooltipRect = el.getBoundingClientRect(); const pad = 8; if (tooltipRect.left < pad) { el.style.transform = `translateX(${pad - tooltipRect.left}px)`; } else if (tooltipRect.right > window.innerWidth - pad) { el.style.transform = `translateX(${window.innerWidth - pad - tooltipRect.right}px)`; } else { el.style.transform = ""; } }, [visible, position]); return ( <> {React.cloneElement(children, { ref: triggerRef, onMouseEnter: show, onMouseLeave: hide, onFocus: show, onBlur: hide, })} {visible && position && (
{content}
)} ); }