"use client"; import React, { useState, useRef, useEffect } from "react"; import { motion, AnimatePresence } from "motion/react"; import { cn } from "@/lib/utils"; export const Tooltip = ({ content, children, containerClassName, }: { content: string | React.ReactNode; children: React.ReactNode; containerClassName?: string; }) => { const [isVisible, setIsVisible] = useState(false); const [mouse, setMouse] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [height, setHeight] = useState(0); const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0, }); const contentRef = useRef(null); const containerRef = useRef(null); useEffect(() => { if (isVisible && contentRef.current) { setHeight(contentRef.current.scrollHeight); } }, [isVisible, content]); const calculatePosition = (mouseX: number, mouseY: number) => { if (!contentRef.current || !containerRef.current) return { x: mouseX + 12, y: mouseY + 12 }; const tooltip = contentRef.current; const container = containerRef.current; const containerRect = container.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Get tooltip dimensions const tooltipWidth = 240; // min-w-[15rem] = 240px const tooltipHeight = tooltip.scrollHeight; // Calculate absolute position relative to viewport const absoluteX = containerRect.left + mouseX; const absoluteY = containerRect.top + mouseY; let finalX = mouseX + 12; let finalY = mouseY + 12; // Check if tooltip goes beyond right edge if (absoluteX + 12 + tooltipWidth > viewportWidth) { finalX = mouseX - tooltipWidth - 12; } // Check if tooltip goes beyond left edge if (absoluteX + finalX < 0) { finalX = -containerRect.left + 12; } // Check if tooltip goes beyond bottom edge if (absoluteY + 12 + tooltipHeight > viewportHeight) { finalY = mouseY - tooltipHeight - 12; } // Check if tooltip goes beyond top edge if (absoluteY + finalY < 0) { finalY = -containerRect.top + 12; } return { x: finalX, y: finalY }; }; const updateMousePosition = (mouseX: number, mouseY: number) => { setMouse({ x: mouseX, y: mouseY }); const newPosition = calculatePosition(mouseX, mouseY); setPosition(newPosition); }; const handleMouseEnter = (e: React.MouseEvent) => { setIsVisible(true); const rect = e.currentTarget.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; updateMousePosition(mouseX, mouseY); }; const handleMouseLeave = () => { setMouse({ x: 0, y: 0 }); setPosition({ x: 0, y: 0 }); setIsVisible(false); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isVisible) return; const rect = e.currentTarget.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; updateMousePosition(mouseX, mouseY); }; const handleTouchStart = (e: React.TouchEvent) => { const touch = e.touches[0]; const rect = e.currentTarget.getBoundingClientRect(); const mouseX = touch.clientX - rect.left; const mouseY = touch.clientY - rect.top; updateMousePosition(mouseX, mouseY); setIsVisible(true); }; const handleTouchEnd = () => { // Delay hiding to allow for tap interaction setTimeout(() => { setIsVisible(false); setMouse({ x: 0, y: 0 }); setPosition({ x: 0, y: 0 }); }, 2000); }; const handleClick = (e: React.MouseEvent) => { // Toggle visibility on click for mobile devices if (window.matchMedia("(hover: none)").matches) { e.preventDefault(); if (isVisible) { setIsVisible(false); setMouse({ x: 0, y: 0 }); setPosition({ x: 0, y: 0 }); } else { const rect = e.currentTarget.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; updateMousePosition(mouseX, mouseY); setIsVisible(true); } } }; // Update position when tooltip becomes visible or content changes useEffect(() => { if (isVisible && contentRef.current) { const newPosition = calculatePosition(mouse.x, mouse.y); setPosition(newPosition); } }, [isVisible, height, mouse.x, mouse.y]); return (
{children} {isVisible && (
{content}
)}
); };