{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitives-animate-tooltip",
  "title": "Tooltip",
  "description": "An animated tooltip that shows contextual info on hover or focus and smoothly glides to the next element without disappearing between transitions.",
  "dependencies": [
    "motion",
    "@floating-ui/react"
  ],
  "registryDependencies": [
    "@unlumen-ui/primitives-animate-slot",
    "@unlumen-ui/lib-get-strict-context"
  ],
  "files": [
    {
      "path": "registry/primitives/animate/tooltip/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  LayoutGroup,\n  type Transition,\n  type HTMLMotionProps,\n} from \"motion/react\";\nimport {\n  useFloating,\n  autoUpdate,\n  offset as floatingOffset,\n  flip,\n  shift,\n  arrow as floatingArrow,\n  FloatingPortal,\n  FloatingArrow,\n  type UseFloatingReturn,\n} from \"@floating-ui/react\";\n\nimport { getStrictContext } from \"@/lib/get-strict-context\";\nimport { Slot, type WithAsChild } from \"@/components/unlumen-ui/primitives/animate/slot\";\n\ntype Side = \"top\" | \"bottom\" | \"left\" | \"right\";\ntype Align = \"start\" | \"center\" | \"end\";\n\ntype TooltipData = {\n  contentProps: HTMLMotionProps<\"div\">;\n  contentAsChild: boolean;\n  rect: DOMRect;\n  side: Side;\n  sideOffset: number;\n  align: Align;\n  alignOffset: number;\n  id: string;\n};\n\ntype GlobalTooltipContextType = {\n  showTooltip: (data: TooltipData) => void;\n  hideTooltip: () => void;\n  hideImmediate: () => void;\n  currentTooltip: TooltipData | null;\n  transition: Transition;\n  globalId: string;\n  setReferenceEl: (el: HTMLElement | null) => void;\n  referenceElRef: React.RefObject<HTMLElement | null>;\n};\n\nconst [GlobalTooltipProvider, useGlobalTooltip] =\n  getStrictContext<GlobalTooltipContextType>(\"GlobalTooltipProvider\");\n\ntype TooltipContextType = {\n  props: HTMLMotionProps<\"div\">;\n  setProps: React.Dispatch<React.SetStateAction<HTMLMotionProps<\"div\">>>;\n  asChild: boolean;\n  setAsChild: React.Dispatch<React.SetStateAction<boolean>>;\n  side: Side;\n  sideOffset: number;\n  align: Align;\n  alignOffset: number;\n  id: string;\n};\n\nconst [LocalTooltipProvider, useTooltip] = getStrictContext<TooltipContextType>(\n  \"LocalTooltipProvider\",\n);\n\ntype TooltipPosition = { x: number; y: number };\n\nfunction getResolvedSide(placement: Side | `${Side}-${Align}`) {\n  if (placement.includes(\"-\")) {\n    return placement.split(\"-\")[0] as Side;\n  }\n  return placement as Side;\n}\n\nfunction initialFromSide(side: Side): Partial<Record<\"x\" | \"y\", number>> {\n  if (side === \"top\") return { y: 15 };\n  if (side === \"bottom\") return { y: -15 };\n  if (side === \"left\") return { x: 15 };\n  return { x: -15 };\n}\n\ntype TooltipProviderProps = {\n  children: React.ReactNode;\n  id?: string;\n  openDelay?: number;\n  closeDelay?: number;\n  transition?: Transition;\n};\n\nfunction TooltipProvider({\n  children,\n  id,\n  openDelay = 700,\n  closeDelay = 300,\n  transition = { type: \"spring\", stiffness: 300, damping: 35 },\n}: TooltipProviderProps) {\n  const globalId = React.useId();\n  const [currentTooltip, setCurrentTooltip] =\n    React.useState<TooltipData | null>(null);\n  const timeoutRef = React.useRef<number | null>(null);\n  const lastCloseTimeRef = React.useRef<number>(0);\n  const referenceElRef = React.useRef<HTMLElement | null>(null);\n\n  const showTooltip = React.useCallback(\n    (data: TooltipData) => {\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\n      if (currentTooltip !== null) {\n        setCurrentTooltip(data);\n        return;\n      }\n      const now = Date.now();\n      const delay = now - lastCloseTimeRef.current < closeDelay ? 0 : openDelay;\n      timeoutRef.current = window.setTimeout(\n        () => setCurrentTooltip(data),\n        delay,\n      );\n    },\n    [openDelay, closeDelay, currentTooltip],\n  );\n\n  const hideTooltip = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    timeoutRef.current = window.setTimeout(() => {\n      setCurrentTooltip(null);\n      lastCloseTimeRef.current = Date.now();\n    }, closeDelay);\n  }, [closeDelay]);\n\n  const hideImmediate = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    setCurrentTooltip(null);\n    lastCloseTimeRef.current = Date.now();\n  }, []);\n\n  const setReferenceEl = React.useCallback((el: HTMLElement | null) => {\n    referenceElRef.current = el;\n  }, []);\n\n  React.useEffect(() => {\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") hideImmediate();\n    };\n    window.addEventListener(\"keydown\", onKeyDown, true);\n    window.addEventListener(\"scroll\", hideImmediate, true);\n    window.addEventListener(\"resize\", hideImmediate, true);\n    return () => {\n      window.removeEventListener(\"keydown\", onKeyDown, true);\n      window.removeEventListener(\"scroll\", hideImmediate, true);\n      window.removeEventListener(\"resize\", hideImmediate, true);\n    };\n  }, [hideImmediate]);\n\n  return (\n    <GlobalTooltipProvider\n      value={{\n        showTooltip,\n        hideTooltip,\n        hideImmediate,\n        currentTooltip,\n        transition,\n        globalId: id ?? globalId,\n        setReferenceEl,\n        referenceElRef,\n      }}\n    >\n      <LayoutGroup>{children}</LayoutGroup>\n      <TooltipOverlay />\n    </GlobalTooltipProvider>\n  );\n}\n\ntype RenderedTooltipContextType = {\n  side: Side;\n  align: Align;\n  open: boolean;\n};\n\nconst [RenderedTooltipProvider, useRenderedTooltip] =\n  getStrictContext<RenderedTooltipContextType>(\"RenderedTooltipContext\");\n\ntype FloatingContextType = {\n  context: UseFloatingReturn[\"context\"];\n  arrowRef: React.RefObject<SVGSVGElement | null>;\n};\n\nconst [FloatingProvider, useFloatingContext] =\n  getStrictContext<FloatingContextType>(\"FloatingContext\");\n\nconst MotionTooltipArrow = motion.create(FloatingArrow);\n\ntype TooltipArrowProps = Omit<\n  React.ComponentProps<typeof MotionTooltipArrow>,\n  \"context\"\n> & {\n  withTransition?: boolean;\n};\n\nfunction TooltipArrow({\n  ref,\n  withTransition = true,\n  ...props\n}: TooltipArrowProps) {\n  const { side, align, open } = useRenderedTooltip();\n  const { context, arrowRef } = useFloatingContext();\n  const { transition, globalId } = useGlobalTooltip();\n  React.useImperativeHandle(ref, () => arrowRef.current as SVGSVGElement);\n\n  const deg = { top: 0, right: 90, bottom: 180, left: -90 }[side];\n\n  return (\n    <MotionTooltipArrow\n      ref={arrowRef}\n      context={context}\n      data-state={open ? \"open\" : \"closed\"}\n      data-side={side}\n      data-align={align}\n      data-slot=\"tooltip-arrow\"\n      style={{ rotate: deg }}\n      layoutId={withTransition ? `tooltip-arrow-${globalId}` : undefined}\n      transition={withTransition ? transition : undefined}\n      {...props}\n    />\n  );\n}\n\ntype TooltipPortalProps = React.ComponentProps<typeof FloatingPortal>;\n\nfunction TooltipPortal(props: TooltipPortalProps) {\n  return <FloatingPortal {...props} />;\n}\n\nfunction TooltipOverlay() {\n  const { currentTooltip, transition, globalId, referenceElRef } =\n    useGlobalTooltip();\n\n  const [rendered, setRendered] = React.useState<{\n    data: TooltipData | null;\n    open: boolean;\n  }>({ data: null, open: false });\n\n  const arrowRef = React.useRef<SVGSVGElement | null>(null);\n\n  const side = rendered.data?.side ?? \"top\";\n  const align = rendered.data?.align ?? \"center\";\n\n  const { refs, x, y, strategy, context, update } = useFloating({\n    placement: align === \"center\" ? side : `${side}-${align}`,\n    whileElementsMounted: autoUpdate,\n    middleware: [\n      floatingOffset({\n        mainAxis: rendered.data?.sideOffset ?? 0,\n        crossAxis: rendered.data?.alignOffset ?? 0,\n      }),\n      flip(),\n      shift({ padding: 8 }),\n      floatingArrow({ element: arrowRef }),\n    ],\n  });\n\n  React.useEffect(() => {\n    if (currentTooltip) {\n      setRendered({ data: currentTooltip, open: true });\n    } else {\n      setRendered((p) => (p.data ? { ...p, open: false } : p));\n    }\n  }, [currentTooltip]);\n\n  React.useLayoutEffect(() => {\n    if (referenceElRef.current) {\n      refs.setReference(referenceElRef.current);\n      update();\n    }\n  }, [referenceElRef, refs, update, rendered.data]);\n\n  const ready = x != null && y != null;\n  const Component = rendered.data?.contentAsChild ? Slot : motion.div;\n  const resolvedSide = getResolvedSide(context.placement);\n\n  return (\n    <AnimatePresence mode=\"wait\">\n      {rendered.data && ready && (\n        <TooltipPortal>\n          <div\n            ref={refs.setFloating}\n            data-slot=\"tooltip-overlay\"\n            data-side={resolvedSide}\n            data-align={rendered.data.align}\n            data-state={rendered.open ? \"open\" : \"closed\"}\n            style={{\n              position: strategy,\n              top: 0,\n              left: 0,\n              zIndex: 50,\n              transform: `translate3d(${x!}px, ${y!}px, 0)`,\n            }}\n          >\n            <FloatingProvider value={{ context, arrowRef }}>\n              <RenderedTooltipProvider\n                value={{\n                  side: resolvedSide,\n                  align: rendered.data.align,\n                  open: rendered.open,\n                }}\n              >\n                <Component\n                  data-slot=\"tooltip-content\"\n                  data-side={resolvedSide}\n                  data-align={rendered.data.align}\n                  data-state={rendered.open ? \"open\" : \"closed\"}\n                  layoutId={`tooltip-content-${globalId}`}\n                  initial={{\n                    opacity: 0,\n                    scale: 0,\n                    ...initialFromSide(rendered.data.side),\n                  }}\n                  animate={\n                    rendered.open\n                      ? { opacity: 1, scale: 1, x: 0, y: 0 }\n                      : {\n                          opacity: 0,\n                          scale: 0,\n                          ...initialFromSide(rendered.data.side),\n                        }\n                  }\n                  exit={{\n                    opacity: 0,\n                    scale: 0,\n                    ...initialFromSide(rendered.data.side),\n                  }}\n                  onAnimationComplete={() => {\n                    if (!rendered.open)\n                      setRendered({ data: null, open: false });\n                  }}\n                  transition={transition}\n                  {...rendered.data.contentProps}\n                  style={{\n                    position: \"relative\",\n                    ...(rendered.data.contentProps?.style || {}),\n                  }}\n                />\n              </RenderedTooltipProvider>\n            </FloatingProvider>\n          </div>\n        </TooltipPortal>\n      )}\n    </AnimatePresence>\n  );\n}\n\ntype TooltipProps = {\n  children: React.ReactNode;\n  side?: Side;\n  sideOffset?: number;\n  align?: Align;\n  alignOffset?: number;\n};\n\nfunction Tooltip({\n  children,\n  side = \"top\",\n  sideOffset = 0,\n  align = \"center\",\n  alignOffset = 0,\n}: TooltipProps) {\n  const id = React.useId();\n  const [props, setProps] = React.useState<HTMLMotionProps<\"div\">>({});\n  const [asChild, setAsChild] = React.useState(false);\n\n  return (\n    <LocalTooltipProvider\n      value={{\n        props,\n        setProps,\n        asChild,\n        setAsChild,\n        side,\n        sideOffset,\n        align,\n        alignOffset,\n        id,\n      }}\n    >\n      {children}\n    </LocalTooltipProvider>\n  );\n}\n\ntype TooltipContentProps = WithAsChild<HTMLMotionProps<\"div\">>;\n\nfunction shallowEqualWithoutChildren(\n  a?: HTMLMotionProps<\"div\">,\n  b?: HTMLMotionProps<\"div\">,\n) {\n  if (a === b) return true;\n  if (!a || !b) return false;\n  const keysA = Object.keys(a).filter((k) => k !== \"children\");\n  const keysB = Object.keys(b).filter((k) => k !== \"children\");\n  if (keysA.length !== keysB.length) return false;\n  for (const k of keysA) {\n    // @ts-expect-error index\n    if (a[k] !== b[k]) return false;\n  }\n  return true;\n}\n\nfunction TooltipContent({ asChild = false, ...props }: TooltipContentProps) {\n  const { setProps, setAsChild } = useTooltip();\n  const lastPropsRef = React.useRef<HTMLMotionProps<\"div\"> | undefined>(\n    undefined,\n  );\n\n  React.useEffect(() => {\n    if (!shallowEqualWithoutChildren(lastPropsRef.current, props)) {\n      lastPropsRef.current = props;\n      setProps(props);\n    }\n  }, [props, setProps]);\n\n  React.useEffect(() => {\n    setAsChild(asChild);\n  }, [asChild, setAsChild]);\n\n  return null;\n}\n\ntype TooltipTriggerProps = WithAsChild<HTMLMotionProps<\"div\">>;\n\nfunction TooltipTrigger({\n  ref,\n  onMouseEnter,\n  onMouseLeave,\n  onFocus,\n  onBlur,\n  onPointerDown,\n  asChild = false,\n  ...props\n}: TooltipTriggerProps) {\n  const {\n    props: contentProps,\n    asChild: contentAsChild,\n    side,\n    sideOffset,\n    align,\n    alignOffset,\n    id,\n  } = useTooltip();\n  const {\n    showTooltip,\n    hideTooltip,\n    hideImmediate,\n    currentTooltip,\n    setReferenceEl,\n  } = useGlobalTooltip();\n\n  const triggerRef = React.useRef<HTMLDivElement>(null);\n  React.useImperativeHandle(ref, () => triggerRef.current as HTMLDivElement);\n\n  const suppressNextFocusRef = React.useRef(false);\n\n  const handleOpen = React.useCallback(() => {\n    if (!triggerRef.current) return;\n    setReferenceEl(triggerRef.current);\n    const rect = triggerRef.current.getBoundingClientRect();\n    showTooltip({\n      contentProps,\n      contentAsChild,\n      rect,\n      side,\n      sideOffset,\n      align,\n      alignOffset,\n      id,\n    });\n  }, [\n    showTooltip,\n    setReferenceEl,\n    contentProps,\n    contentAsChild,\n    side,\n    sideOffset,\n    align,\n    alignOffset,\n    id,\n  ]);\n\n  const handlePointerDown = React.useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      onPointerDown?.(e);\n      if (currentTooltip?.id === id) {\n        suppressNextFocusRef.current = true;\n        hideImmediate();\n        Promise.resolve().then(() => {\n          suppressNextFocusRef.current = false;\n        });\n      }\n    },\n    [onPointerDown, currentTooltip?.id, id, hideImmediate],\n  );\n\n  const handleMouseEnter = React.useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      onMouseEnter?.(e);\n      handleOpen();\n    },\n    [handleOpen, onMouseEnter],\n  );\n\n  const handleMouseLeave = React.useCallback(\n    (e: React.MouseEvent<HTMLDivElement>) => {\n      onMouseLeave?.(e);\n      hideTooltip();\n    },\n    [hideTooltip, onMouseLeave],\n  );\n\n  const handleFocus = React.useCallback(\n    (e: React.FocusEvent<HTMLDivElement>) => {\n      onFocus?.(e);\n      if (suppressNextFocusRef.current) return;\n      handleOpen();\n    },\n    [handleOpen, onFocus],\n  );\n\n  const handleBlur = React.useCallback(\n    (e: React.FocusEvent<HTMLDivElement>) => {\n      onBlur?.(e);\n      hideTooltip();\n    },\n    [hideTooltip, onBlur],\n  );\n\n  const Component = asChild ? Slot : motion.div;\n\n  return (\n    <Component\n      ref={triggerRef}\n      onPointerDown={handlePointerDown}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      onFocus={handleFocus}\n      onBlur={handleBlur}\n      data-slot=\"tooltip-trigger\"\n      data-side={side}\n      data-align={align}\n      data-state={currentTooltip?.id === id ? \"open\" : \"closed\"}\n      {...props}\n    />\n  );\n}\n\nexport {\n  TooltipProvider,\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n  TooltipArrow,\n  useGlobalTooltip,\n  useTooltip,\n  type TooltipProviderProps,\n  type TooltipProps,\n  type TooltipContentProps,\n  type TooltipTriggerProps,\n  type TooltipArrowProps,\n  type TooltipPosition,\n  type GlobalTooltipContextType,\n  type TooltipContextType,\n};\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/primitives/animate/tooltip.tsx"
    }
  ],
  "type": "registry:ui"
}