import { useState, useEffect, useRef, useCallback } from 'react'; type Position = 'top' | 'bottom' | 'left' | 'right'; export function useTooltip( position: Position, trigger: 'hover' | 'click' | 'focus', delay: number, disabled: boolean, ) { const [visible, setVisible] = useState(false); const [isMounted, setIsMounted] = useState(false); const [calculatedPosition, setCalculatedPosition] = useState(position); const [tooltipStyle, setTooltipStyle] = useState({}); const timeoutRef = useRef(null); const hideTimeoutRef = useRef(null); const wrapperRef = useRef(null); const tooltipRef = useRef(null); const calculatePosition = useCallback(() => { if (!wrapperRef.current || !tooltipRef.current || !visible) return; const wrapperRect = wrapperRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const margin = 8; let newPosition = position; let offsetX = 0; let offsetY = 0; switch (position) { case 'top': if (wrapperRect.top - tooltipRect.height - margin < 0) { newPosition = 'bottom'; } break; case 'bottom': if (wrapperRect.bottom + tooltipRect.height + margin > viewportHeight) { newPosition = 'top'; } break; case 'left': if (wrapperRect.left - tooltipRect.width - margin < 0) { newPosition = 'right'; } break; case 'right': if (wrapperRect.right + tooltipRect.width + margin > viewportWidth) { newPosition = 'left'; } break; } if (newPosition === 'top' || newPosition === 'bottom') { const centerX = wrapperRect.left + wrapperRect.width / 2; const tooltipHalfWidth = tooltipRect.width / 2; const minX = margin; const maxX = viewportWidth - margin; if (centerX - tooltipHalfWidth < minX) { offsetX = minX - (centerX - tooltipHalfWidth); } else if (centerX + tooltipHalfWidth > maxX) { offsetX = maxX - (centerX + tooltipHalfWidth); } } else { const centerY = wrapperRect.top + wrapperRect.height / 2; const tooltipHalfHeight = tooltipRect.height / 2; const minY = margin; const maxY = viewportHeight - margin; if (centerY - tooltipHalfHeight < minY) { offsetY = minY - (centerY - tooltipHalfHeight); } else if (centerY + tooltipHalfHeight > maxY) { offsetY = maxY - (centerY + tooltipHalfHeight); } } setCalculatedPosition(newPosition); setTooltipStyle({ ...(offsetX !== 0 && { transform: `translate(calc(-50% + ${offsetX}px), 0)`, }), ...(offsetY !== 0 && { transform: `translate(0, calc(-50% + ${offsetY}px))`, }), }); }, [position, visible]); useEffect(() => { if (visible) { calculatePosition(); } }, [visible, calculatePosition]); useEffect(() => { if (isMounted && visible) { calculatePosition(); } }, [isMounted, visible, calculatePosition]); const showTooltip = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { setVisible(true); setIsMounted(true); }, delay); }, [delay]); const hideTooltip = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } hideTimeoutRef.current = setTimeout(() => { setVisible(false); }, 100); }, []); const handleClick = useCallback(() => { if (trigger === 'click') { setVisible((v) => { const next = !v; if (next) setIsMounted(true); return next; }); } }, [trigger]); const triggerProps = trigger === 'hover' ? { onMouseEnter: showTooltip, onMouseLeave: hideTooltip } : trigger === 'click' ? { onClick: handleClick } : { onFocus: showTooltip, onBlur: hideTooltip }; useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); }; }, []); return { visible, isMounted, calculatedPosition, tooltipStyle, wrapperRef, tooltipRef, triggerProps: disabled ? {} : triggerProps, }; } export const positionClasses: Record< Position, string > = { top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', left: 'right-full top-1/2 -translate-y-1/2 mr-2', right: 'left-full top-1/2 -translate-y-1/2 ml-2', }; export const arrowClasses: Record = { top: 'top-full left-1/2 -translate-x-1/2 border-t-kodo-ink border-l-transparent border-r-transparent border-b-transparent', bottom: 'bottom-full left-1/2 -translate-x-1/2 border-b-kodo-ink border-l-transparent border-r-transparent border-t-transparent', left: 'left-full top-1/2 -translate-y-1/2 border-l-kodo-ink border-t-transparent border-b-transparent border-r-transparent', right: 'right-full top-1/2 -translate-y-1/2 border-r-kodo-ink border-t-transparent border-b-transparent border-l-transparent', };