{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Spring-animated tooltip with multiple placement options","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\n\nexport type AnimatedTooltipPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface AnimatedTooltipProps {\n  /** The trigger element the tooltip is anchored to */\n  children: ReactNode;\n  /** Additional CSS class names for the tooltip container */\n  className?: string;\n  /** Content displayed inside the tooltip */\n  content: ReactNode;\n  /** Delay in milliseconds before the tooltip appears */\n  delay?: number;\n  /** Placement of the tooltip relative to the trigger */\n  placement?: AnimatedTooltipPlacement;\n}\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst placementStyles: Record<AnimatedTooltipPlacement, string> = {\n  bottom: \"top-full left-1/2 -translate-x-1/2 mt-2\",\n  left: \"right-full top-1/2 -translate-y-1/2 mr-2\",\n  right: \"left-full top-1/2 -translate-y-1/2 ml-2\",\n  top: \"bottom-full left-1/2 -translate-x-1/2 mb-2\",\n};\n\nconst arrowStyles: Record<AnimatedTooltipPlacement, string> = {\n  bottom:\n    \"bottom-full left-1/2 -translate-x-1/2 border-b-foreground border-x-transparent border-t-transparent\",\n  left: \"left-full top-1/2 -translate-y-1/2 border-l-foreground border-y-transparent border-r-transparent\",\n  right:\n    \"right-full top-1/2 -translate-y-1/2 border-r-foreground border-y-transparent border-l-transparent\",\n  top: \"top-full left-1/2 -translate-x-1/2 border-t-foreground border-x-transparent border-b-transparent\",\n};\n\nconst arrowBorderSize: Record<AnimatedTooltipPlacement, string> = {\n  bottom: \"border-4\",\n  left: \"border-4\",\n  right: \"border-4\",\n  top: \"border-4\",\n};\n\nconst getInitialTransform = (\n  placement: AnimatedTooltipPlacement\n): { opacity: number; scale: number; x: number; y: number } => {\n  const base = { opacity: 0, scale: 0.95, x: 0, y: 0 };\n  switch (placement) {\n    case \"top\":\n      return { ...base, y: 4 };\n    case \"bottom\":\n      return { ...base, y: -4 };\n    case \"left\":\n      return { ...base, x: 4 };\n    case \"right\":\n      return { ...base, x: -4 };\n  }\n};\n\nconst AnimatedTooltip = ({\n  content,\n  placement = \"top\",\n  delay = 0,\n  children,\n  className,\n}: AnimatedTooltipProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isVisible, setIsVisible] = useState(false);\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const tooltipId = useId();\n  const delayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const show = useCallback(() => {\n    if (delay > 0) {\n      delayTimerRef.current = setTimeout(() => {\n        setIsVisible(true);\n      }, delay);\n    } else {\n      setIsVisible(true);\n    }\n  }, [delay]);\n\n  const hide = useCallback(() => {\n    if (delayTimerRef.current !== null) {\n      clearTimeout(delayTimerRef.current);\n      delayTimerRef.current = null;\n    }\n    setIsVisible(false);\n  }, []);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        hide();\n      }\n    },\n    [hide]\n  );\n\n  const initialTransform = getInitialTransform(placement);\n\n  return (\n    <span\n      className=\"relative inline-flex\"\n      onBlur={hide}\n      onFocus={show}\n      onKeyDown={handleKeyDown}\n      onMouseEnter={isHoverDevice ? show : undefined}\n      onMouseLeave={isHoverDevice ? hide : undefined}\n    >\n      <span aria-describedby={isVisible ? tooltipId : undefined}>\n        {children}\n      </span>\n\n      <AnimatePresence>\n        {isVisible ? (\n          <motion.span\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 1, scale: 1, x: 0, y: 0 }\n            }\n            className={cn(\n              \"absolute z-50 w-max max-w-xs rounded-md bg-foreground px-3 py-1.5 text-background text-sm shadow-md\",\n              placementStyles[placement],\n              className\n            )}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : {\n                    ...initialTransform,\n                    transition: { duration: 0.15 },\n                  }\n            }\n            id={tooltipId}\n            initial={shouldReduceMotion ? { opacity: 0 } : initialTransform}\n            role=\"tooltip\"\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n          >\n            {content}\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"absolute block h-0 w-0\",\n                arrowBorderSize[placement],\n                arrowStyles[placement]\n              )}\n            />\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n    </span>\n  );\n};\n\nexport default AnimatedTooltip;\n","path":"index.tsx","target":"components/smoothui/animated-tooltip/index.tsx","type":"registry:ui"}],"name":"animated-tooltip","registryDependencies":[],"title":"Animated Tooltip","type":"registry:ui"}