{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fluid-tooltip",
  "type": "registry:ui",
  "title": "Fluid Tooltip",
  "description": "A grouped Base UI tooltip system with a deliberate first appearance and fast directional handoffs between related controls.",
  "files": [
    {
      "path": "registry/sonaui/fluid-tooltip/fluid-tooltip.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Tooltip } from \"@base-ui/react/tooltip\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type CSSProperties,\n  createContext,\n  type FocusEventHandler,\n  forwardRef,\n  type KeyboardEventHandler,\n  type PointerEventHandler,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  type RefObject,\n  useContext,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/sona-utils\";\n\ntype FluidTooltipOrientation = \"horizontal\" | \"vertical\" | \"auto\";\ntype FluidTooltipSide = \"top\" | \"right\" | \"bottom\" | \"left\";\ntype FluidTooltipAlign = \"start\" | \"center\" | \"end\";\ntype FluidTooltipDirection = -1 | 0 | 1;\n\ninterface FluidTooltipPayload {\n  id: string;\n  contentRef: RefObject<ReactNode>;\n  side: FluidTooltipSide;\n  align: FluidTooltipAlign;\n  sideOffset: number;\n  showArrowRef: RefObject<boolean>;\n  contentClassNameRef: RefObject<string | undefined>;\n}\n\ninterface FluidTooltipGroupContextValue {\n  handle: Tooltip.Handle<FluidTooltipPayload>;\n  disabled: boolean;\n  defaultSide: FluidTooltipSide;\n  direction: FluidTooltipDirection;\n  keyboardNavigation: boolean;\n  registerPointerTarget: (element: HTMLElement) => void;\n  registerKeyboardTarget: () => void;\n}\n\ninterface FluidTooltipRootContextValue {\n  id: string;\n  contentRef: RefObject<ReactNode>;\n  disabled: boolean;\n  side?: FluidTooltipSide;\n  align: FluidTooltipAlign;\n  sideOffset: number;\n  showArrowRef: RefObject<boolean>;\n  contentClassNameRef: RefObject<string | undefined>;\n}\n\nconst GroupContext = createContext<FluidTooltipGroupContextValue | null>(null);\nconst RootContext = createContext<FluidTooltipRootContextValue | null>(null);\n\nconst tokenStyle = {\n  \"--fluid-tooltip-surface\": \"var(--foreground)\",\n  \"--fluid-tooltip-label\": \"var(--background)\",\n  \"--fluid-tooltip-shadow\": \"rgb(0 0 0 / 0.28)\",\n} as CSSProperties;\n\nfunction useGroupContext(component: string) {\n  const context = useContext(GroupContext);\n  if (!context) {\n    throw new Error(`${component} must be used inside FluidTooltip.Group.`);\n  }\n  return context;\n}\n\nfunction useRootContext(component: string) {\n  const context = useContext(RootContext);\n  if (!context) {\n    throw new Error(`${component} must be used inside FluidTooltip.Root.`);\n  }\n  return context;\n}\n\nexport interface FluidTooltipGroupProps {\n  /** Related tooltip roots rendered inside the group. */\n  children: ReactNode;\n  /** Axis used to calculate directional content entry. @default \"auto\" */\n  orientation?: FluidTooltipOrientation;\n  /** Delay before the first pointer tooltip opens, in milliseconds. @default 350 */\n  openDelay?: number;\n  /** Grace period before a pointer tooltip closes, in milliseconds. @default 100 */\n  closeDelay?: number;\n  /** Disables tooltip behavior for every trigger in the group. @default false */\n  disabled?: boolean;\n  /** Additional CSS classes for the positioned tooltip surface. @default undefined */\n  className?: string;\n}\n\nexport function FluidTooltipGroup({\n  children,\n  orientation = \"auto\",\n  openDelay = 350,\n  closeDelay = 100,\n  disabled = false,\n  className,\n}: FluidTooltipGroupProps) {\n  const handle = useMemo(() => Tooltip.createHandle<FluidTooltipPayload>(), []);\n  const shouldReduceMotion = useReducedMotion();\n  const previousCenter = useRef<{ x: number; y: number } | null>(null);\n  const [direction, setDirection] = useState<FluidTooltipDirection>(0);\n  const [keyboardNavigation, setKeyboardNavigation] = useState(false);\n\n  const context = useMemo<FluidTooltipGroupContextValue>(\n    () => ({\n      handle,\n      disabled,\n      defaultSide: orientation === \"vertical\" ? \"right\" : \"top\",\n      direction,\n      keyboardNavigation,\n      registerPointerTarget(element) {\n        const bounds = element.getBoundingClientRect();\n        const center = {\n          x: bounds.left + bounds.width / 2,\n          y: bounds.top + bounds.height / 2,\n        };\n        const previous = previousCenter.current;\n\n        if (!previous) {\n          setDirection(0);\n        } else {\n          const deltaX = center.x - previous.x;\n          const deltaY = center.y - previous.y;\n          const resolvedAxis =\n            orientation === \"horizontal\"\n              ? \"x\"\n              : orientation === \"vertical\"\n                ? \"y\"\n                : Math.abs(deltaX) >= Math.abs(deltaY)\n                  ? \"x\"\n                  : \"y\";\n          const delta = resolvedAxis === \"x\" ? deltaX : deltaY;\n          setDirection(delta === 0 ? 0 : delta > 0 ? 1 : -1);\n        }\n\n        previousCenter.current = center;\n        setKeyboardNavigation(false);\n      },\n      registerKeyboardTarget() {\n        previousCenter.current = null;\n        setDirection(0);\n        setKeyboardNavigation(true);\n      },\n    }),\n    [disabled, direction, handle, keyboardNavigation, orientation],\n  );\n\n  return (\n    <Tooltip.Provider\n      delay={Math.max(0, openDelay)}\n      closeDelay={Math.max(0, closeDelay)}\n      timeout={50}\n    >\n      <GroupContext.Provider value={context}>{children}</GroupContext.Provider>\n\n      <Tooltip.Root\n        handle={handle}\n        disabled={disabled}\n        onOpenChange={(open) => {\n          if (!open) {\n            previousCenter.current = null;\n            setDirection(0);\n          }\n        }}\n      >\n        {({ payload }) => (\n          <Tooltip.Portal>\n            {payload ? (\n              <Tooltip.Positioner\n                align={payload.align}\n                className={cn(\n                  \"z-9999 h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)]\",\n                  shouldReduceMotion\n                    ? \"transition-none\"\n                    : \"transition-[top,left,right,bottom,transform] duration-200 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none\",\n                )}\n                collisionPadding={8}\n                side={payload.side}\n                sideOffset={payload.sideOffset}\n              >\n                <Tooltip.Popup\n                  className={cn(\n                    \"relative origin-[var(--transform-origin)] rounded-lg bg-[var(--fluid-tooltip-surface)] text-[12px] font-medium leading-none text-[var(--fluid-tooltip-label)] shadow-[0_8px_24px_-8px_var(--fluid-tooltip-shadow)]\",\n                    \"h-[var(--popup-height,auto)] w-[var(--popup-width,auto)] max-w-[var(--available-width)]\",\n                    shouldReduceMotion\n                      ? \"transition-none data-ending-style:opacity-0 data-starting-style:scale-100 data-starting-style:translate-y-0 data-starting-style:opacity-100\"\n                      : \"transition-[width,height,transform,opacity] duration-200 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-100 data-starting-style:scale-[0.96] data-starting-style:translate-y-1 data-starting-style:opacity-0 data-instant:transition-none\",\n                    className,\n                    payload.contentClassNameRef.current,\n                  )}\n                  style={tokenStyle}\n                >\n                  <Tooltip.Viewport\n                    className={cn(\n                      \"relative box-border h-full w-full overflow-clip px-2 py-1\",\n                      \"[&_[data-previous]]:w-[calc(var(--popup-width)-1rem)] [&_[data-previous]]:translate-x-0 [&_[data-previous]]:opacity-0 [&_[data-previous]]:pointer-events-none [&_[data-previous]]:transition-none\",\n                      shouldReduceMotion\n                        ? \"[&_[data-current]]:w-[calc(var(--popup-width)-1rem)] [&_[data-current]]:translate-x-0 [&_[data-current]]:translate-y-0 [&_[data-current]]:opacity-100 [&_[data-current]]:transition-none\"\n                        : \"[&_[data-current]]:w-[calc(var(--popup-width)-1rem)] [&_[data-current]]:translate-x-0 [&_[data-current]]:opacity-100 [&_[data-current]]:transition-[translate,opacity] [&_[data-current]]:duration-[200ms,120ms] data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-2 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-2 data-[activation-direction~='up']:[&_[data-current][data-starting-style]]:-translate-y-2 data-[activation-direction~='down']:[&_[data-current][data-starting-style]]:translate-y-2 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-2 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-2 data-[activation-direction~='up']:[&_[data-previous][data-ending-style]]:translate-y-2 data-[activation-direction~='down']:[&_[data-previous][data-ending-style]]:-translate-y-2 [[data-instant]_&_[data-previous]]:transition-none [[data-instant]_&_[data-current]]:transition-none\",\n                    )}\n                  >\n                    {payload.contentRef.current}\n                  </Tooltip.Viewport>\n\n                  {payload.showArrowRef.current ? (\n                    <Tooltip.Arrow className=\"absolute size-2 rotate-45 bg-[var(--fluid-tooltip-surface)] data-[side=bottom]:-top-1 data-[side=left]:-right-1 data-[side=right]:-left-1 data-[side=top]:-bottom-1\" />\n                  ) : null}\n                </Tooltip.Popup>\n              </Tooltip.Positioner>\n            ) : null}\n          </Tooltip.Portal>\n        )}\n      </Tooltip.Root>\n    </Tooltip.Provider>\n  );\n}\n\nexport interface FluidTooltipRootProps {\n  /** Stable identifier used to key directional content transitions. */\n  id: string;\n  /** Trigger and content parts associated with this tooltip. */\n  children: ReactNode;\n  /** Preferred side of the trigger. Overrides the group default when provided. @default undefined */\n  side?: FluidTooltipSide;\n  /** Alignment relative to the trigger. @default \"center\" */\n  align?: FluidTooltipAlign;\n  /** Distance between the trigger and tooltip, in pixels. @default 8 */\n  sideOffset?: number;\n  /** Disables this tooltip without disabling its trigger. @default false */\n  disabled?: boolean;\n}\n\nexport function FluidTooltipRoot({\n  id,\n  children,\n  side,\n  align = \"center\",\n  sideOffset = 8,\n  disabled = false,\n}: FluidTooltipRootProps) {\n  const contentRef = useRef<ReactNode>(null);\n  const showArrowRef = useRef(true);\n  const contentClassNameRef = useRef<string | undefined>(undefined);\n  const context = useMemo<FluidTooltipRootContextValue>(\n    () => ({\n      id,\n      contentRef,\n      disabled,\n      side,\n      align,\n      sideOffset,\n      showArrowRef,\n      contentClassNameRef,\n    }),\n    [align, disabled, id, side, sideOffset],\n  );\n\n  return (\n    <RootContext.Provider value={context}>{children}</RootContext.Provider>\n  );\n}\n\nexport interface FluidTooltipTriggerProps\n  extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  /** Existing button or link used as the actual trigger. */\n  children: ReactElement;\n  /** Uses the child element without introducing a wrapper. @default true */\n  asChild?: boolean;\n  /** Keeps the tooltip open when the trigger is activated. @default false */\n  keepOpenOnClick?: boolean;\n}\n\nexport const FluidTooltipTrigger = forwardRef<\n  HTMLElement,\n  FluidTooltipTriggerProps\n>(function FluidTooltipTrigger(\n  {\n    children,\n    asChild = true,\n    keepOpenOnClick = false,\n    onPointerEnter,\n    onFocus,\n    onKeyDown,\n    ...props\n  },\n  ref,\n) {\n  const group = useGroupContext(\"FluidTooltip.Trigger\");\n  const root = useRootContext(\"FluidTooltip.Trigger\");\n\n  const payload: FluidTooltipPayload = {\n    id: root.id,\n    contentRef: root.contentRef,\n    side: root.side ?? group.defaultSide,\n    align: root.align,\n    sideOffset: root.sideOffset,\n    showArrowRef: root.showArrowRef,\n    contentClassNameRef: root.contentClassNameRef,\n  };\n\n  const handlePointerEnter: PointerEventHandler<HTMLElement> = (event) => {\n    if (event.pointerType !== \"touch\") {\n      group.registerPointerTarget(event.currentTarget);\n    }\n    onPointerEnter?.(event);\n  };\n  const handleFocus: FocusEventHandler<HTMLElement> = (event) => {\n    group.registerKeyboardTarget();\n    onFocus?.(event);\n  };\n  const handleKeyDown: KeyboardEventHandler<HTMLElement> = (event) => {\n    group.registerKeyboardTarget();\n    onKeyDown?.(event);\n  };\n\n  return (\n    <Tooltip.Trigger\n      {...props}\n      ref={ref as Ref<HTMLButtonElement>}\n      closeOnClick={!keepOpenOnClick}\n      disabled={group.disabled || root.disabled}\n      handle={group.handle}\n      onFocus={handleFocus}\n      onKeyDown={handleKeyDown}\n      onPointerEnter={handlePointerEnter}\n      payload={payload}\n      render={asChild ? children : undefined}\n    >\n      {asChild ? undefined : children}\n    </Tooltip.Trigger>\n  );\n});\n\nexport interface FluidTooltipContentProps {\n  /** Short, non-interactive tooltip label. */\n  children: ReactNode;\n  /** Additional CSS classes for this tooltip's surface. @default undefined */\n  className?: string;\n  /** Shows the arrow connecting the surface to its trigger. @default true */\n  showArrow?: boolean;\n}\n\nexport function FluidTooltipContent({\n  children,\n  className,\n  showArrow = true,\n}: FluidTooltipContentProps) {\n  const root = useRootContext(\"FluidTooltip.Content\");\n  useLayoutEffect(() => {\n    root.contentRef.current = children;\n    root.contentClassNameRef.current = className;\n    root.showArrowRef.current = showArrow;\n  }, [children, className, root, showArrow]);\n  return null;\n}\n\nexport const FluidTooltip = {\n  Group: FluidTooltipGroup,\n  Root: FluidTooltipRoot,\n  Trigger: FluidTooltipTrigger,\n  Content: FluidTooltipContent,\n};\n\nexport default FluidTooltip;\n",
      "target": "components/ui/fluid-tooltip/fluid-tooltip.tsx"
    }
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}