{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:ui",
  "title": "Tooltip",
  "description": "Tooltip is a brief supplementary label for a trigger, shown on keyboard focus or pointer hover.",
  "author": "Skrewww",
  "dependencies": [],
  "registryDependencies": [
    "@skrewww/foundation"
  ],
  "docs": "Host requirements (assumed already present, not installed by this command): react, react-dom.",
  "files": [
    {
      "path": "components/ui/Tooltip.tsx",
      "content": "/** @skrewww-component tooltip */\n\"use client\";\n\nimport {\n  cloneElement,\n  isValidElement,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/cn\";\nimport { computeTooltipPosition } from \"@/components/ui/internal/tooltip-position\";\nimport type { TooltipPlacement } from \"@/components/ui/internal/tooltip-position\";\nimport { mergeRefs } from \"@/components/ui/internal/assign-ref\";\nimport { useIsClient } from \"@/components/ui/internal/useIsClient\";\nimport { useTooltipController } from \"@/components/ui/internal/useTooltipController\";\nimport { useOverlayEscape } from \"@/components/ui/internal/useOverlayEscape\";\nimport styles from \"@/components/ui/tooltip.module.css\";\n\ntype TooltipTriggerProps = {\n  \"aria-describedby\"?: string;\n  onMouseEnter?: React.MouseEventHandler<HTMLElement>;\n  onMouseLeave?: React.MouseEventHandler<HTMLElement>;\n  onFocus?: React.FocusEventHandler<HTMLElement>;\n  onBlur?: React.FocusEventHandler<HTMLElement>;\n  ref?: React.Ref<HTMLElement>;\n};\n\ntype TooltipTriggerBindProps = {\n  child: React.ReactElement<TooltipTriggerProps>;\n  triggerRef: React.RefObject<HTMLElement | null>;\n  describedBy?: string;\n  scheduleOpen: () => void;\n  scheduleClose: () => void;\n  openImmediately: () => void;\n};\n\nfunction TooltipTriggerBind({\n  child,\n  triggerRef,\n  describedBy,\n  scheduleOpen,\n  scheduleClose,\n  openImmediately,\n}: TooltipTriggerBindProps) {\n  return cloneElement(child, {\n    ref: mergeRefs(triggerRef, child.props.ref),\n    \"aria-describedby\": describedBy || undefined,\n    onMouseEnter: (event: React.MouseEvent<HTMLElement>) => {\n      child.props.onMouseEnter?.(event);\n      scheduleOpen();\n    },\n    onMouseLeave: (event: React.MouseEvent<HTMLElement>) => {\n      child.props.onMouseLeave?.(event);\n      scheduleClose();\n    },\n    onFocus: (event: React.FocusEvent<HTMLElement>) => {\n      child.props.onFocus?.(event);\n      openImmediately();\n    },\n    onBlur: (event: React.FocusEvent<HTMLElement>) => {\n      child.props.onBlur?.(event);\n      scheduleClose();\n    },\n  });\n}\n\nexport type TooltipProps = {\n  content: React.ReactNode;\n  placement?: TooltipPlacement;\n  openDelay?: number;\n  closeDelay?: number;\n  disabled?: boolean;\n  className?: string;\n  children: React.ReactElement<TooltipTriggerProps>;\n};\n\nexport function Tooltip({\n  content,\n  placement = \"top\",\n  openDelay,\n  closeDelay,\n  disabled = false,\n  className,\n  children,\n}: TooltipProps) {\n  const tooltipId = useId();\n  const triggerRef = useRef<HTMLElement | null>(null);\n  const tooltipRef = useRef<HTMLDivElement | null>(null);\n  const mounted = useIsClient();\n  const [coords, setCoords] = useState({ top: 0, left: 0 });\n  const [resolvedPlacement, setResolvedPlacement] = useState(placement);\n  const {\n    open,\n    scheduleOpen,\n    scheduleClose,\n    openImmediately,\n    closeImmediately,\n  } = useTooltipController({ openDelay, closeDelay, disabled });\n\n  useLayoutEffect(() => {\n    if (!open || !triggerRef.current || !tooltipRef.current) return;\n\n    const triggerRect = triggerRef.current.getBoundingClientRect();\n    const tooltipRect = tooltipRef.current.getBoundingClientRect();\n    const next = computeTooltipPosition(\n      triggerRect,\n      tooltipRect,\n      placement,\n      parseFloat(getComputedStyle(document.documentElement).getPropertyValue(\"--tooltip-offset\")) ||\n        8,\n    );\n    setCoords({ top: next.top, left: next.left });\n    setResolvedPlacement(next.placement);\n  }, [open, placement, content]);\n\n  const stackZIndex = useOverlayEscape(open, closeImmediately);\n\n  if (!isValidElement(children)) {\n    return children;\n  }\n\n  const child = children as React.ReactElement<TooltipTriggerProps>;\n\n  const describedBy =\n    open && content\n      ? [child.props[\"aria-describedby\"], tooltipId].filter(Boolean).join(\" \")\n      : child.props[\"aria-describedby\"];\n\n  const trigger = (\n    <TooltipTriggerBind\n      child={child}\n      triggerRef={triggerRef}\n      describedBy={describedBy}\n      scheduleOpen={scheduleOpen}\n      scheduleClose={scheduleClose}\n      openImmediately={openImmediately}\n    />\n  );\n\n  const tooltipNode =\n    open && mounted && content ? (\n      <div\n        ref={tooltipRef}\n        id={tooltipId}\n        role=\"tooltip\"\n        className={cn(styles.tooltip, styles[resolvedPlacement], className)}\n        style={{ top: coords.top, left: coords.left, zIndex: stackZIndex }}\n        onMouseEnter={openImmediately}\n        onMouseLeave={scheduleClose}\n      >\n        {content}\n      </div>\n    ) : null;\n\n  return (\n    <>\n      {trigger}\n      {mounted && tooltipNode ? createPortal(tooltipNode, document.body) : null}\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/Tooltip.tsx"
    },
    {
      "path": "components/ui/tooltip.module.css",
      "content": ".tooltip {\n  position: fixed;\n  z-index: var(--tooltip-z-index);\n  max-width: var(--tooltip-max-width);\n  padding: var(--tooltip-padding-y) var(--tooltip-padding-x);\n  border-radius: var(--tooltip-radius);\n  /* Figma Feedback/Tooltip 2034:25628 is fixed color/neutral/900 with no\n     Surface participation. Do not invent Glass lightening or blur here. */\n  background: var(--tooltip-surface);\n  color: var(--tooltip-text);\n  box-shadow: var(--tooltip-shadow);\n  font-size: 0.75rem;\n  line-height: 1.35;\n  pointer-events: auto;\n  animation: tooltip-enter var(--tooltip-motion-duration) ease;\n}\n\n.top,\n.bottom,\n.left,\n.right {\n  transform: none;\n}\n\n@keyframes tooltip-enter {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .tooltip {\n    animation: none;\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/tooltip.module.css"
    },
    {
      "path": "lib/cn.ts",
      "content": "export function cn(...classes: Array<string | false | null | undefined>): string {\n  return classes.filter(Boolean).join(\" \");\n}\n",
      "type": "registry:lib",
      "target": "~/lib/cn.ts"
    },
    {
      "path": "components/ui/internal/tooltip-position.ts",
      "content": "export type TooltipPlacement = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport type TooltipPosition = {\n  top: number;\n  left: number;\n  placement: TooltipPlacement;\n};\n\nexport function getOppositePlacement(placement: TooltipPlacement): TooltipPlacement {\n  if (placement === \"top\") return \"bottom\";\n  if (placement === \"bottom\") return \"top\";\n  if (placement === \"left\") return \"right\";\n  return \"left\";\n}\n\nexport function computeTooltipPosition(\n  triggerRect: DOMRect,\n  tooltipRect: DOMRect,\n  preferred: TooltipPlacement,\n  offset: number,\n): TooltipPosition {\n  const placements: TooltipPlacement[] = [preferred, \"top\", \"bottom\", \"right\", \"left\"];\n  const viewportPadding = 8;\n\n  for (const placement of placements) {\n    let top = 0;\n    let left = 0;\n\n    if (placement === \"top\") {\n      top = triggerRect.top - tooltipRect.height - offset;\n      left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;\n    } else if (placement === \"bottom\") {\n      top = triggerRect.bottom + offset;\n      left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;\n    } else if (placement === \"left\") {\n      top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;\n      left = triggerRect.left - tooltipRect.width - offset;\n    } else {\n      top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;\n      left = triggerRect.right + offset;\n    }\n\n    const fits =\n      top >= viewportPadding &&\n      left >= viewportPadding &&\n      top + tooltipRect.height <= window.innerHeight - viewportPadding &&\n      left + tooltipRect.width <= window.innerWidth - viewportPadding;\n\n    if (fits) {\n      return {\n        top: Math.max(viewportPadding, top),\n        left: Math.max(viewportPadding, left),\n        placement,\n      };\n    }\n  }\n\n  return {\n    top: Math.max(viewportPadding, triggerRect.bottom + offset),\n    left: Math.max(\n      viewportPadding,\n      triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2,\n    ),\n    placement: preferred,\n  };\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/tooltip-position.ts"
    },
    {
      "path": "components/ui/internal/assign-ref.ts",
      "content": "import type { Ref } from \"react\";\n\n/** Write a value into a React ref prop (callback or object) without touching render. */\nexport function assignRef<T>(ref: Ref<T> | undefined | null, value: T) {\n  if (!ref) return;\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n  ref.current = value;\n}\n\nexport function mergeRefs<T>(\n  ...refs: Array<Ref<T> | undefined | null>\n): (value: T) => void {\n  return (value) => {\n    for (const ref of refs) {\n      assignRef(ref, value);\n    }\n  };\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/assign-ref.ts"
    },
    {
      "path": "components/ui/internal/useIsClient.ts",
      "content": "import { useSyncExternalStore } from \"react\";\n\nconst subscribe = () => () => {};\n\n/**\n * Client-only gate for portals and browser APIs.\n * Hydration uses the server snapshot (`false`); the client snapshot is `true`.\n */\nexport function useIsClient() {\n  return useSyncExternalStore(subscribe, () => true, () => false);\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/useIsClient.ts"
    },
    {
      "path": "components/ui/internal/useTooltipController.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\n\ntype UseTooltipControllerOptions = {\n  openDelay?: number;\n  closeDelay?: number;\n  disabled?: boolean;\n};\n\nexport function useTooltipController({\n  openDelay = 300,\n  closeDelay = 100,\n  disabled = false,\n}: UseTooltipControllerOptions = {}) {\n  const [open, setOpen] = useState(false);\n  const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const clearTimers = useCallback(() => {\n    if (openTimer.current) {\n      clearTimeout(openTimer.current);\n      openTimer.current = null;\n    }\n    if (closeTimer.current) {\n      clearTimeout(closeTimer.current);\n      closeTimer.current = null;\n    }\n  }, []);\n\n  useEffect(() => clearTimers, [clearTimers]);\n\n  const scheduleOpen = useCallback(() => {\n    if (disabled) return;\n    clearTimers();\n    openTimer.current = setTimeout(() => setOpen(true), openDelay);\n  }, [clearTimers, disabled, openDelay]);\n\n  const scheduleClose = useCallback(() => {\n    clearTimers();\n    closeTimer.current = setTimeout(() => setOpen(false), closeDelay);\n  }, [clearTimers, closeDelay]);\n\n  const openImmediately = useCallback(() => {\n    if (disabled) return;\n    clearTimers();\n    setOpen(true);\n  }, [clearTimers, disabled]);\n\n  const closeImmediately = useCallback(() => {\n    clearTimers();\n    setOpen(false);\n  }, [clearTimers]);\n\n  return {\n    open,\n    scheduleOpen,\n    scheduleClose,\n    openImmediately,\n    closeImmediately,\n  };\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/useTooltipController.ts"
    },
    {
      "path": "components/ui/internal/useOverlayEscape.ts",
      "content": "import { useLayoutEffect, useState } from \"react\";\nimport { registerOverlay } from \"@/components/ui/internal/overlay-stack\";\nimport { useLatestRef } from \"@/components/ui/internal/useLatestRef\";\n\n/**\n * Base z-index for stacked overlay content (Popover/Dialog/Drawer/Tooltip). Each\n * overlay's actual z-index is this base plus its registration order in the shared\n * overlay stack, so an overlay opened later — e.g. a Popover opened from inside an\n * already-open Dialog — always renders above the ones already on screen, regardless\n * of which component type it is. Without this, a Popover's static z-index token could\n * be lower than a Dialog's/Drawer's, rendering it behind them even though it opened\n * on top of the stack (see docs/architecture nested-overlay notes).\n */\nconst OVERLAY_STACK_BASE_Z_INDEX = 60;\n\nexport function useOverlayEscape(\n  active: boolean,\n  onEscape: () => void,\n  options?: { modal?: boolean },\n): number | undefined {\n  const onEscapeRef = useLatestRef(onEscape);\n  const [zIndex, setZIndex] = useState<number | undefined>(undefined);\n\n  useLayoutEffect(() => {\n    // Skip when inactive: every consumer of this hook (Popover/Dialog/Drawer/Tooltip content)\n    // renders null while its own `open`/`active` state is false, so a stale zIndex from a prior\n    // open never reaches the DOM — it gets overwritten with a fresh order-derived value the next\n    // time this effect registers.\n    if (!active) return;\n    const { order, unregister } = registerOverlay(() => onEscapeRef.current(), {\n      modal: options?.modal,\n    });\n    setZIndex(OVERLAY_STACK_BASE_Z_INDEX + order);\n    return unregister;\n  }, [active, onEscapeRef, options?.modal]);\n\n  return zIndex;\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/useOverlayEscape.ts"
    },
    {
      "path": "components/ui/internal/overlay-stack.ts",
      "content": "type OverlayRegistration = {\n  id: symbol;\n  order: number;\n  onEscape: () => void;\n  modal: boolean;\n};\n\nexport type OverlayHandle = {\n  /** Monotonically increasing registration order — later-registered overlays get higher values. */\n  order: number;\n  unregister: () => void;\n};\n\nlet orderCounter = 0;\nconst overlayStack: OverlayRegistration[] = [];\nlet escapeListenerBound = false;\n\nfunction handleDocumentEscape(event: KeyboardEvent) {\n  if (event.key !== \"Escape\") return;\n  const top = overlayStack[overlayStack.length - 1];\n  if (!top) return;\n  event.preventDefault();\n  event.stopPropagation();\n  top.onEscape();\n}\n\nfunction ensureEscapeListener() {\n  if (escapeListenerBound || typeof document === \"undefined\") return;\n  document.addEventListener(\"keydown\", handleDocumentEscape);\n  escapeListenerBound = true;\n}\n\nexport function registerOverlay(\n  onEscape: () => void,\n  options?: { modal?: boolean },\n): OverlayHandle {\n  ensureEscapeListener();\n\n  const registration: OverlayRegistration = {\n    id: Symbol(\"overlay\"),\n    order: ++orderCounter,\n    onEscape,\n    modal: options?.modal ?? false,\n  };\n\n  overlayStack.push(registration);\n  overlayStack.sort((left, right) => left.order - right.order);\n\n  return {\n    order: registration.order,\n    unregister: () => {\n      const index = overlayStack.findIndex((entry) => entry.id === registration.id);\n      if (index >= 0) overlayStack.splice(index, 1);\n    },\n  };\n}\n\nexport function getTopOverlay(): OverlayRegistration | undefined {\n  return overlayStack[overlayStack.length - 1];\n}\n\nexport function clearOverlayStackForTests() {\n  overlayStack.length = 0;\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/overlay-stack.ts"
    },
    {
      "path": "components/ui/internal/useLatestRef.ts",
      "content": "import { useLayoutEffect, useRef } from \"react\";\n\n/**\n * Keep a ref pointed at the latest value without retriggering an effect that\n * only needs to read it later (event handlers, observers, overlay callbacks).\n * The write happens after commit so it does not read/write refs during render.\n */\nexport function useLatestRef<T>(value: T) {\n  const ref = useRef(value);\n  useLayoutEffect(() => {\n    ref.current = value;\n  }, [value]);\n  return ref;\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/useLatestRef.ts"
    }
  ]
}
