1import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
2import { createContext, use, useEffect, useRef, type ComponentProps, type RefObject } from "react";
3
4import { cn } from "~/lib/utils";
5import { useNativeTitlebarHeight } from "~/lib/windowControlsOverlay";
6
7// Base UI's default, restated because a padding object zeroes the sides it omits.
8const COLLISION_PADDING = 5;
9
10const TooltipProvider = TooltipPrimitive.Provider;
11
12type TooltipActionsRef = RefObject<TooltipPrimitive.Root.Actions | null>;
13const TooltipHoverContext = createContext<TooltipActionsRef | null>(null);
14const TooltipScrollContext = createContext<RefObject<{
15 trigger: HTMLElement;
16 actionsRef: TooltipActionsRef;
17} | null> | null>(null);
18
19/** Dismisses hovered descendants on real scroll events without rerendering the timeline. */
20function TooltipScrollDismissArea({ onScrollCapture, ...props }: ComponentProps<"div">) {
21 const hovered = useRef<{ trigger: HTMLElement; actionsRef: TooltipActionsRef } | null>(null);
22 return (
23 <TooltipScrollContext value={hovered}>
24 <div
25 {...props}
26 onScrollCapture={(event) => {
27 onScrollCapture?.(event);
28 const tooltip = hovered.current;
29 if (!tooltip || tooltip.trigger.contains(tooltip.trigger.ownerDocument.activeElement)) {
30 return;
31 }
32 hovered.current = null;
33 // Base UI also cancels delayed hover opens through this action.
34 tooltip.actionsRef.current?.close();
35 }}
36 />
37 </TooltipScrollContext>
38 );
39}
40
41function Tooltip<Payload>(props: TooltipPrimitive.Root.Props<Payload>) {
42 const hovered = use(TooltipScrollContext);
43 const localActionsRef = useRef<TooltipPrimitive.Root.Actions | null>(null);
44 const actionsRef = props.actionsRef ?? localActionsRef;
45 useEffect(
46 () => () => {
47 if (hovered?.current?.actionsRef === actionsRef) hovered.current = null;
48 },
49 [actionsRef, hovered],
50 );
51
52 if (!hovered) return <TooltipPrimitive.Root {...props} />;
53 return (
54 <TooltipHoverContext value={actionsRef}>
55 <TooltipPrimitive.Root
56 {...props}
57 actionsRef={actionsRef}
58 onOpenChange={(open, details) => {
59 props.onOpenChange?.(open, details);
60 if (!open && !details.isCanceled && hovered.current?.actionsRef === actionsRef) {
61 hovered.current = null;
62 }
63 }}
64 />
65 </TooltipHoverContext>
66 );
67}
68
69function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
70 const hovered = use(TooltipScrollContext);
71 const actionsRef = use(TooltipHoverContext);
72 if (!hovered || !actionsRef) {
73 return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
74 }
75 return (
76 <TooltipPrimitive.Trigger
77 data-slot="tooltip-trigger"
78 {...props}
79 onMouseEnter={(event) => {
80 props.onMouseEnter?.(event);
81 hovered.current = { trigger: event.currentTarget, actionsRef };
82 }}
83 />
84 );
85}
86
87/**
88 * Treats the native titlebar strip as off-screen, so a tooltip that would open
89 * under the window controls flips to its other side. Rendered inside the
90 * portal so only open tooltips track the titlebar.
91 */
92function TooltipPositioner(props: TooltipPrimitive.Positioner.Props) {
93 const titlebarHeight = useNativeTitlebarHeight();
94 return (
95 <TooltipPrimitive.Positioner
96 className="pointer-events-none z-[140] h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
97 collisionPadding={{
98 top: COLLISION_PADDING + titlebarHeight,
99 right: COLLISION_PADDING,
100 bottom: COLLISION_PADDING,
101 left: COLLISION_PADDING,
102 }}
103 data-slot="tooltip-positioner"
104 {...props}
105 />
106 );
107}
108
109function TooltipPopup({
110 className,
111 align = "center",
112 sideOffset = 4,
113 side = "top",
114 variant = "default",
115 anchor,
116 children,
117 ...props
118}: TooltipPrimitive.Popup.Props & {
119 align?: TooltipPrimitive.Positioner.Props["align"];
120 side?: TooltipPrimitive.Positioner.Props["side"];
121 sideOffset?: TooltipPrimitive.Positioner.Props["sideOffset"];
122 /** `code` renders monospace content that breaks anywhere, for paths and commands. */
123 variant?: "default" | "glass" | "code";
124 anchor?: TooltipPrimitive.Positioner.Props["anchor"];
125}) {
126 return (
127 <TooltipPrimitive.Portal>
128 <TooltipPositioner align={align} anchor={anchor} side={side} sideOffset={sideOffset}>
129 <TooltipPrimitive.Popup
130 className={cn(
131 "relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) text-balance rounded-md text-popover-foreground text-xs transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:duration-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
132 variant === "glass"
133 ? "dropdown-glass shadow-xl shadow-black/25 before:hidden"
134 : "border bg-popover not-dark:bg-clip-padding shadow-md/5",
135 // One wrap width for prose; code dumps get more room and break anywhere.
136 variant === "code"
137 ? "max-w-120 wrap-anywhere text-left font-mono text-[11px] leading-relaxed"
138 : "max-w-80 wrap-anywhere whitespace-normal leading-snug",
139 className,
140 )}
141 data-slot="tooltip-popup"
142 {...props}
143 >
144 <TooltipPrimitive.Viewport
145 className="relative size-full overflow-clip px-(--viewport-inline-padding) py-1 [--viewport-inline-padding:--spacing(2)] data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:truncate **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity"
146 data-slot="tooltip-viewport"
147 >
148 {children}
149 </TooltipPrimitive.Viewport>
150 </TooltipPrimitive.Popup>
151 </TooltipPositioner>
152 </TooltipPrimitive.Portal>
153 );
154}
155
156export { TooltipProvider, Tooltip, TooltipTrigger, TooltipPopup, TooltipScrollDismissArea };