beez-ui 0.5.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@
2
2
  import { c as _c } from "react/compiler-runtime";
3
3
  import * as React from "react";
4
4
  import { Select as SelectPrimitive } from "radix-ui";
5
+ import { GlideSlot } from "../motion/glide-slot.js";
5
6
  import { MotionSlot } from "../motion/motion-slot.js";
6
7
  import { cn } from "../lib/utils.js";
7
8
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
@@ -115,7 +116,7 @@ function SelectTrigger(t0) {
115
116
  const size = t1 === undefined ? "default" : t1;
116
117
  let t2;
117
118
  if ($[5] !== className) {
118
- t2 = cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className);
119
+ t2 = cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border data-[state=open]:*:data-[slot=select-icon]:rotate-180 border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className);
119
120
  $[5] = className;
120
121
  $[6] = t2;
121
122
  } else {
@@ -125,7 +126,13 @@ function SelectTrigger(t0) {
125
126
  if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
126
127
  t3 = /* @__PURE__ */ _jsx(SelectPrimitive.Icon, {
127
128
  asChild: true,
128
- children: /* @__PURE__ */ _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 text-muted-foreground" })
129
+ children: /* @__PURE__ */ _jsx(MotionSlot, {
130
+ kind: "rotate",
131
+ children: /* @__PURE__ */ _jsx(ChevronDownIcon, {
132
+ "data-slot": "select-icon",
133
+ className: "pointer-events-none size-4 text-muted-foreground"
134
+ })
135
+ })
129
136
  });
130
137
  $[7] = t3;
131
138
  } else {
@@ -234,18 +241,21 @@ function SelectContent(t0) {
234
241
  if ($[17] !== align || $[18] !== position || $[19] !== props || $[20] !== t3 || $[21] !== t5 || $[22] !== t9) {
235
242
  t11 = /* @__PURE__ */ _jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ _jsx(MotionSlot, {
236
243
  kind: "surface",
237
- children: /* @__PURE__ */ _jsxs(SelectPrimitive.Content, {
238
- "data-slot": "select-content",
239
- "data-align-trigger": t3,
240
- className: t5,
241
- position,
242
- align,
243
- ...props,
244
- children: [
245
- t6,
246
- t9,
247
- t10
248
- ]
244
+ children: /* @__PURE__ */ _jsx(GlideSlot, {
245
+ preset: "menu",
246
+ children: /* @__PURE__ */ _jsxs(SelectPrimitive.Content, {
247
+ "data-slot": "select-content",
248
+ "data-align-trigger": t3,
249
+ className: t5,
250
+ position,
251
+ align,
252
+ ...props,
253
+ children: [
254
+ t6,
255
+ t9,
256
+ t10
257
+ ]
258
+ })
249
259
  })
250
260
  }) });
251
261
  $[17] = align;
@@ -334,7 +344,10 @@ function SelectItem(t0) {
334
344
  if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
335
345
  t2 = /* @__PURE__ */ _jsx("span", {
336
346
  className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center",
337
- children: /* @__PURE__ */ _jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ _jsx(CheckIcon, { className: "pointer-events-none" }) })
347
+ children: /* @__PURE__ */ _jsx(MotionSlot, {
348
+ kind: "item-check",
349
+ children: /* @__PURE__ */ _jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ _jsx(CheckIcon, { className: "pointer-events-none" }) })
350
+ })
338
351
  });
339
352
  $[6] = t2;
340
353
  } else {
@@ -351,7 +364,7 @@ function SelectItem(t0) {
351
364
  let t4;
352
365
  if ($[9] !== props || $[10] !== t1 || $[11] !== t3) {
353
366
  t4 = /* @__PURE__ */ _jsx(MotionSlot, {
354
- kind: "field",
367
+ kind: "list-item",
355
368
  children: /* @__PURE__ */ _jsxs(SelectPrimitive.Item, {
356
369
  "data-slot": "select-item",
357
370
  className: t1,
@@ -5,6 +5,7 @@ import { cva } from "class-variance-authority";
5
5
  import { Slot } from "radix-ui";
6
6
  import { useIsMobile } from "../hooks/use-mobile.js";
7
7
  import { useSidebarPersistence, saveSidebarPreference } from "../hooks/use-sidebar-persistence.js";
8
+ import { GlideSlot } from "../motion/glide-slot.js";
8
9
  import { MotionSlot } from "../motion/motion-slot.js";
9
10
  import { cn } from "../lib/utils.js";
10
11
  import { SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME } from "../constants/sidebar.js";
@@ -937,11 +938,14 @@ function SidebarMenu(t0) {
937
938
  }
938
939
  let t2;
939
940
  if ($[5] !== props || $[6] !== t1) {
940
- t2 = /* @__PURE__ */ _jsx("ul", {
941
- "data-slot": "sidebar-menu",
942
- "data-sidebar": "menu",
943
- className: t1,
944
- ...props
941
+ t2 = /* @__PURE__ */ _jsx(GlideSlot, {
942
+ preset: "sidebar",
943
+ children: /* @__PURE__ */ _jsx("ul", {
944
+ "data-slot": "sidebar-menu",
945
+ "data-sidebar": "menu",
946
+ className: t1,
947
+ ...props
948
+ })
945
949
  });
946
950
  $[5] = props;
947
951
  $[6] = t1;
@@ -1056,7 +1060,7 @@ function SidebarMenuButton(t0) {
1056
1060
  let t6;
1057
1061
  if ($[12] !== Comp || $[13] !== isActive || $[14] !== props || $[15] !== size || $[16] !== t5) {
1058
1062
  t6 = /* @__PURE__ */ _jsx(MotionSlot, {
1059
- kind: "press",
1063
+ kind: "subtle-press",
1060
1064
  children: /* @__PURE__ */ _jsx(Comp, {
1061
1065
  "data-slot": "sidebar-menu-button",
1062
1066
  "data-sidebar": "menu-button",
@@ -1396,7 +1400,7 @@ function SidebarMenuSubButton(t0) {
1396
1400
  let t5;
1397
1401
  if ($[8] !== Comp || $[9] !== isActive || $[10] !== props || $[11] !== size || $[12] !== t4) {
1398
1402
  t5 = /* @__PURE__ */ _jsx(MotionSlot, {
1399
- kind: "press",
1403
+ kind: "subtle-press",
1400
1404
  children: /* @__PURE__ */ _jsx(Comp, {
1401
1405
  "data-slot": "sidebar-menu-sub-button",
1402
1406
  "data-sidebar": "menu-sub-button",
@@ -3,6 +3,7 @@ import { c as _c } from "react/compiler-runtime";
3
3
  import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Tabs as TabsPrimitive } from "radix-ui";
6
+ import { GlideSlot } from "../motion/glide-slot.js";
6
7
  import { MotionSlot } from "../motion/motion-slot.js";
7
8
  import { cn } from "../lib/utils.js";
8
9
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -83,11 +84,14 @@ function TabsList(t0) {
83
84
  }
84
85
  let t3;
85
86
  if ($[7] !== props || $[8] !== t2 || $[9] !== variant) {
86
- t3 = /* @__PURE__ */ _jsx(TabsPrimitive.List, {
87
- "data-slot": "tabs-list",
88
- "data-variant": variant,
89
- className: t2,
90
- ...props
87
+ t3 = /* @__PURE__ */ _jsx(GlideSlot, {
88
+ preset: "tabs",
89
+ children: /* @__PURE__ */ _jsx(TabsPrimitive.List, {
90
+ "data-slot": "tabs-list",
91
+ "data-variant": variant,
92
+ className: t2,
93
+ ...props
94
+ })
91
95
  });
92
96
  $[7] = props;
93
97
  $[8] = t2;
@@ -113,7 +117,7 @@ function TabsTrigger(t0) {
113
117
  }
114
118
  let t1;
115
119
  if ($[3] !== className) {
116
- t1 = cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className);
120
+ t1 = cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100 data-glide-target:after:opacity-0! data-glide-freeze:after:transition-none", className);
117
121
  $[3] = className;
118
122
  $[4] = t1;
119
123
  } else {
@@ -122,7 +126,7 @@ function TabsTrigger(t0) {
122
126
  let t2;
123
127
  if ($[5] !== props || $[6] !== t1) {
124
128
  t2 = /* @__PURE__ */ _jsx(MotionSlot, {
125
- kind: "press",
129
+ kind: "subtle-press",
126
130
  children: /* @__PURE__ */ _jsx(TabsPrimitive.Trigger, {
127
131
  "data-slot": "tabs-trigger",
128
132
  className: t1,
@@ -161,7 +165,7 @@ function TabsContent(t0) {
161
165
  let t2;
162
166
  if ($[5] !== props || $[6] !== t1) {
163
167
  t2 = /* @__PURE__ */ _jsx(MotionSlot, {
164
- kind: "fade",
168
+ kind: "content",
165
169
  children: /* @__PURE__ */ _jsx(TabsPrimitive.Content, {
166
170
  "data-slot": "tabs-content",
167
171
  className: t1,
@@ -138,7 +138,7 @@ function TooltipContent(t0) {
138
138
  let t5;
139
139
  if ($[8] !== children || $[9] !== props || $[10] !== sideOffset || $[11] !== t3) {
140
140
  t5 = /* @__PURE__ */ _jsx(MotionSlot, {
141
- kind: "surface",
141
+ kind: "tooltip",
142
142
  children: /* @__PURE__ */ _jsxs(TooltipPrimitive.Content, {
143
143
  forceMount: true,
144
144
  "data-slot": "tooltip-content",
@@ -1,2 +1,6 @@
1
1
  /** Shares the operating-system preference across CSS-adjacent and JavaScript animations. */
2
2
  export declare const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
3
+ /** Only true hover pointers get hover lifts; touch devices keep phantom hovers after taps. */
4
+ export declare const HOVER_CAPABLE_QUERY = "(hover: hover) and (pointer: fine)";
5
+ /** Converts Motion's second-based timings to the millisecond timings of Web Animations. */
6
+ export declare const MILLISECONDS_PER_SECOND = 1000;
@@ -1,2 +1,6 @@
1
1
  /** Shares the operating-system preference across CSS-adjacent and JavaScript animations. */
2
2
  export const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
3
+ /** Only true hover pointers get hover lifts; touch devices keep phantom hovers after taps. */
4
+ export const HOVER_CAPABLE_QUERY = "(hover: hover) and (pointer: fine)";
5
+ /** Converts Motion's second-based timings to the millisecond timings of Web Animations. */
6
+ export const MILLISECONDS_PER_SECOND = 1e3;
@@ -0,0 +1,66 @@
1
+ import { SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
2
+ /** Which part of the active item carries the highlight that must travel. */
3
+ type GlideSurface = "background" | "after";
4
+ /** Describes the items of one container and how their active state is exposed. */
5
+ export interface GlidePreset {
6
+ itemSelector: string;
7
+ /**
8
+ * Matches the container and any nested container of the same kind, so items that belong to a
9
+ * nested one (a submenu rendered inside its parent menu) are left to that container's glide.
10
+ */
11
+ containerSelector: string;
12
+ /** Attributes whose mutations can change which item is active. */
13
+ attributes: string[];
14
+ isActive: (item: Element) => boolean;
15
+ surface: (container: HTMLElement) => GlideSurface;
16
+ spring: typeof SPRING_LAYOUT | typeof SPRING_TABS;
17
+ }
18
+ export declare const GLIDE_PRESETS: {
19
+ tabs: {
20
+ itemSelector: string;
21
+ containerSelector: string;
22
+ attributes: string[];
23
+ isActive: (item: Element) => boolean;
24
+ surface: (container: HTMLElement) => "after" | "background";
25
+ spring: {
26
+ readonly type: "spring";
27
+ readonly stiffness: 245;
28
+ readonly damping: 36;
29
+ readonly mass: 1.2;
30
+ };
31
+ };
32
+ sidebar: {
33
+ itemSelector: string;
34
+ containerSelector: string;
35
+ attributes: string[];
36
+ isActive: (item: Element) => boolean;
37
+ surface: () => "background";
38
+ spring: {
39
+ readonly type: "spring";
40
+ readonly stiffness: 360;
41
+ readonly damping: 32;
42
+ readonly mass: 0.6;
43
+ };
44
+ };
45
+ menu: {
46
+ itemSelector: string;
47
+ containerSelector: string;
48
+ attributes: string[];
49
+ isActive: (item: Element) => boolean;
50
+ surface: () => "background";
51
+ spring: {
52
+ readonly type: "spring";
53
+ readonly stiffness: 360;
54
+ readonly damping: 32;
55
+ readonly mass: 0.6;
56
+ };
57
+ };
58
+ };
59
+ export type GlidePresetName = keyof typeof GLIDE_PRESETS;
60
+ /** Marks the item whose own highlight is hidden while the travelling copy stands in for it. */
61
+ export declare const GLIDE_TARGET_ATTRIBUTE = "data-glide-target";
62
+ /** Marks items whose transitions, including pseudo-element ones, are suspended during a glide. */
63
+ export declare const GLIDE_FREEZE_ATTRIBUTE = "data-glide-freeze";
64
+ /** Observes the container and glides the highlight whenever the active item changes. */
65
+ export declare function attachGlideIndicator(container: HTMLElement, preset: GlidePreset): () => void;
66
+ export {};
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Glides an active-item indicator between items of a container, like a shared `layoutId`,
3
+ * without adding persistent markup: a transient copy of the target's highlight travels from the
4
+ * previous item and is removed once it lands, leaving the item's own styles in charge again.
5
+ */
6
+ import { animate } from "motion/react";
7
+ import { GLIDE_CONTINUITY_MS, SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
8
+ export const GLIDE_PRESETS = {
9
+ tabs: {
10
+ itemSelector: "[data-slot=\"tabs-trigger\"]",
11
+ containerSelector: "[role=\"tablist\"]",
12
+ attributes: ["data-state"],
13
+ isActive: (item) => item.getAttribute("data-state") === "active",
14
+ surface: (container) => container.getAttribute("data-variant") === "line" ? "after" : "background",
15
+ spring: SPRING_TABS
16
+ },
17
+ sidebar: {
18
+ itemSelector: "[data-slot=\"sidebar-menu-button\"]",
19
+ containerSelector: "[data-slot=\"sidebar-menu\"]",
20
+ attributes: ["data-active"],
21
+ isActive: (item) => item.getAttribute("data-active") === "true",
22
+ surface: () => "background",
23
+ spring: SPRING_LAYOUT
24
+ },
25
+ menu: {
26
+ itemSelector: "[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"],[role=\"option\"]",
27
+ containerSelector: "[role=\"menu\"],[role=\"listbox\"]",
28
+ attributes: ["data-highlighted"],
29
+ isActive: (item) => item.hasAttribute("data-highlighted"),
30
+ surface: () => "background",
31
+ spring: SPRING_LAYOUT
32
+ }
33
+ };
34
+ /** Marks the item whose own highlight is hidden while the travelling copy stands in for it. */
35
+ export const GLIDE_TARGET_ATTRIBUTE = "data-glide-target";
36
+ /** Marks items whose transitions, including pseudo-element ones, are suspended during a glide. */
37
+ export const GLIDE_FREEZE_ATTRIBUTE = "data-glide-freeze";
38
+ /** Converts a viewport rectangle into the container's scrolling coordinate space. */
39
+ function toContainerBox(container, rect) {
40
+ const origin = container.getBoundingClientRect();
41
+ return {
42
+ left: rect.left - origin.left - container.clientLeft + container.scrollLeft,
43
+ top: rect.top - origin.top - container.clientTop + container.scrollTop,
44
+ width: rect.width,
45
+ height: rect.height
46
+ };
47
+ }
48
+ /** Translation an item's own running entrance adds, read from its computed `matrix()`. */
49
+ function inFlightTranslation(item) {
50
+ const values = /^matrix\(([^)]+)\)$/.exec(getComputedStyle(item).transform);
51
+ if (!values) return {
52
+ x: 0,
53
+ y: 0
54
+ };
55
+ const parts = values[1].split(",").map(Number.parseFloat);
56
+ return {
57
+ x: parts[4] ?? 0,
58
+ y: parts[5] ?? 0
59
+ };
60
+ }
61
+ /**
62
+ * Measures the highlight: the item box, or its positioned `::after` bar for line variants.
63
+ * An item still sliding into place (staggered Select options) is measured where it will
64
+ * settle, so the highlight does not land offset and jump when the entrance ends.
65
+ */
66
+ function highlightBox(item, surface) {
67
+ const box = item.getBoundingClientRect();
68
+ const offset = inFlightTranslation(item);
69
+ const rect = {
70
+ left: box.left - offset.x,
71
+ top: box.top - offset.y,
72
+ width: box.width,
73
+ height: box.height
74
+ };
75
+ if (surface === "background") return rect;
76
+ const pseudo = getComputedStyle(item, "::after");
77
+ return {
78
+ left: rect.left + item.clientLeft + (parseFloat(pseudo.left) || 0),
79
+ top: rect.top + item.clientTop + (parseFloat(pseudo.top) || 0),
80
+ width: parseFloat(pseudo.width) || rect.width,
81
+ height: parseFloat(pseudo.height) || 0
82
+ };
83
+ }
84
+ /** Copies only the paint that identifies the highlight, never layout or content. */
85
+ function highlightPaint(item, surface) {
86
+ const style = getComputedStyle(item, surface === "after" ? "::after" : null);
87
+ return {
88
+ backgroundColor: style.backgroundColor,
89
+ borderRadius: style.borderRadius,
90
+ boxShadow: style.boxShadow,
91
+ borderStyle: style.borderStyle,
92
+ borderWidth: style.borderWidth,
93
+ borderColor: style.borderColor,
94
+ boxSizing: "border-box"
95
+ };
96
+ }
97
+ /** Tracks overlapping freezes so the caller-owned inline transition is restored exactly once. */
98
+ const frozenItems = new WeakMap();
99
+ /**
100
+ * Suppresses CSS transitions for one item, so state changes paint instantly; the returned
101
+ * release restores them after two painted frames. Overlapping freezes are reference-counted.
102
+ */
103
+ function freezeTransitions(item) {
104
+ const frozen = frozenItems.get(item) ?? {
105
+ transition: item.style.transition,
106
+ holds: 0
107
+ };
108
+ frozen.holds += 1;
109
+ frozenItems.set(item, frozen);
110
+ item.style.transition = "none";
111
+ item.setAttribute(GLIDE_FREEZE_ATTRIBUTE, "");
112
+ let released = false;
113
+ return () => {
114
+ if (released) return;
115
+ released = true;
116
+ requestAnimationFrame(() => {
117
+ requestAnimationFrame(() => {
118
+ frozen.holds -= 1;
119
+ if (frozen.holds > 0) return;
120
+ frozenItems.delete(item);
121
+ item.style.transition = frozen.transition;
122
+ item.removeAttribute(GLIDE_FREEZE_ATTRIBUTE);
123
+ });
124
+ });
125
+ };
126
+ }
127
+ /** Observes the container and glides the highlight whenever the active item changes. */
128
+ export function attachGlideIndicator(container, preset) {
129
+ const items = () => Array.from(container.querySelectorAll(preset.itemSelector)).filter((item) => item.parentElement?.closest(preset.containerSelector) === container);
130
+ let lastActive = items().find(preset.isActive) ?? null;
131
+ let lastActiveSeenAt = Number.NEGATIVE_INFINITY;
132
+ let overlay = null;
133
+ let animation = null;
134
+ let releaseTarget = null;
135
+ const containerStyle = {
136
+ position: container.style.position,
137
+ isolation: container.style.isolation
138
+ };
139
+ /** Puts the item's own highlight back and removes the travelling copy. */
140
+ function settle() {
141
+ animation?.cancel();
142
+ animation = null;
143
+ overlay?.remove();
144
+ overlay = null;
145
+ releaseTarget?.();
146
+ releaseTarget = null;
147
+ container.style.position = containerStyle.position;
148
+ container.style.isolation = containerStyle.isolation;
149
+ }
150
+ /** Hides the frozen target's highlight so the copy is the only one visible. */
151
+ function hideTarget(item, surface, thaw) {
152
+ item.setAttribute(GLIDE_TARGET_ATTRIBUTE, "");
153
+ const inline = {
154
+ backgroundColor: item.style.getPropertyValue("background-color"),
155
+ backgroundPriority: item.style.getPropertyPriority("background-color"),
156
+ boxShadow: item.style.getPropertyValue("box-shadow"),
157
+ boxShadowPriority: item.style.getPropertyPriority("box-shadow"),
158
+ borderColor: item.style.getPropertyValue("border-color"),
159
+ borderPriority: item.style.getPropertyPriority("border-color")
160
+ };
161
+ if (surface === "background") {
162
+ item.style.setProperty("background-color", "transparent", "important");
163
+ item.style.setProperty("box-shadow", "none", "important");
164
+ item.style.setProperty("border-color", "transparent", "important");
165
+ }
166
+ return () => {
167
+ item.style.setProperty("background-color", inline.backgroundColor, inline.backgroundPriority);
168
+ item.style.setProperty("box-shadow", inline.boxShadow, inline.boxShadowPriority);
169
+ item.style.setProperty("border-color", inline.borderColor, inline.borderPriority);
170
+ // Transitions stay frozen until the restored highlight has painted in place.
171
+ item.removeAttribute(GLIDE_TARGET_ATTRIBUTE);
172
+ thaw();
173
+ };
174
+ }
175
+ /** Starts, or redirects, the glide from the previous highlight to the new active item. */
176
+ function glide(from, to) {
177
+ const surface = preset.surface(container);
178
+ const origin = overlay ? overlay.getBoundingClientRect() : highlightBox(from, surface);
179
+ settle();
180
+ // The previous item drops its highlight instantly: the copy is what leaves it.
181
+ freezeTransitions(from)();
182
+ // Read the target's settled paint before any transition has a chance to start.
183
+ const thawTarget = freezeTransitions(to);
184
+ const destination = toContainerBox(container, highlightBox(to, surface));
185
+ const paint = highlightPaint(to, surface);
186
+ if (destination.width === 0 || destination.height === 0) {
187
+ thawTarget();
188
+ return;
189
+ }
190
+ if (getComputedStyle(container).position === "static") {
191
+ container.style.position = "relative";
192
+ }
193
+ // Negative z-index keeps the copy above the container background and below item content.
194
+ container.style.isolation = "isolate";
195
+ overlay = document.createElement("span");
196
+ overlay.setAttribute("aria-hidden", "true");
197
+ overlay.setAttribute("data-glide-indicator", "");
198
+ Object.assign(overlay.style, paint, {
199
+ position: "absolute",
200
+ pointerEvents: "none",
201
+ zIndex: "-1",
202
+ margin: "0"
203
+ });
204
+ const start = toContainerBox(container, origin);
205
+ container.prepend(overlay);
206
+ releaseTarget = hideTarget(to, surface, thawTarget);
207
+ animation = animate(overlay, {
208
+ left: [`${start.left}px`, `${destination.left}px`],
209
+ top: [`${start.top}px`, `${destination.top}px`],
210
+ width: [`${start.width}px`, `${destination.width}px`],
211
+ height: [`${start.height}px`, `${destination.height}px`]
212
+ }, preset.spring);
213
+ const current = animation;
214
+ void current.then(() => {
215
+ if (animation === current) settle();
216
+ });
217
+ }
218
+ const observer = new MutationObserver(() => {
219
+ // Items can be remounted (a sidebar button wrapped in a tooltip after hydration); the
220
+ // highlight then belongs to the item that replaced the detached one.
221
+ if (lastActive && !lastActive.isConnected) {
222
+ lastActive = items().find(preset.isActive) ?? null;
223
+ lastActiveSeenAt = Number.NEGATIVE_INFINITY;
224
+ }
225
+ const active = items().find(preset.isActive) ?? null;
226
+ const now = performance.now();
227
+ if (active && active === lastActive) return;
228
+ if (!active) {
229
+ if (lastActive && lastActiveSeenAt === Number.NEGATIVE_INFINITY) {
230
+ lastActiveSeenAt = now;
231
+ }
232
+ settle();
233
+ return;
234
+ }
235
+ const previous = lastActive;
236
+ const isContinuous = previous !== null && previous !== active && previous.isConnected && !preset.isActive(previous) && (lastActiveSeenAt === Number.NEGATIVE_INFINITY || now - lastActiveSeenAt <= GLIDE_CONTINUITY_MS);
237
+ lastActive = active;
238
+ lastActiveSeenAt = Number.NEGATIVE_INFINITY;
239
+ if (isContinuous) glide(previous, active);
240
+ else settle();
241
+ });
242
+ observer.observe(container, {
243
+ subtree: true,
244
+ childList: true,
245
+ attributes: true,
246
+ attributeFilter: preset.attributes
247
+ });
248
+ return () => {
249
+ observer.disconnect();
250
+ settle();
251
+ };
252
+ }
@@ -0,0 +1,9 @@
1
+ /** Adds a gliding active-item indicator to the original container, without DOM wrappers. */
2
+ import { type ComponentProps, type ReactElement } from "react";
3
+ import { Slot } from "radix-ui";
4
+ import { type GlidePresetName } from "./glide-indicator.js";
5
+ /** Composes refs through Radix Slot and glides the preset's highlight between its items. */
6
+ export declare function GlideSlot({ children, preset, ref, ...props }: Omit<ComponentProps<typeof Slot.Root>, "children"> & {
7
+ children: ReactElement;
8
+ preset: GlidePresetName;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /** Adds a gliding active-item indicator to the original container, without DOM wrappers. */
4
+ import { useLayoutEffect } from "react";
5
+ import { Slot } from "radix-ui";
6
+ import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
7
+ import { attachGlideIndicator, GLIDE_PRESETS } from "./glide-indicator.js";
8
+ import { useAttachedElement } from "./use-attached-element.js";
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ /** Composes refs through Radix Slot and glides the preset's highlight between its items. */
11
+ export function GlideSlot(t0) {
12
+ const $ = _c(14);
13
+ let children;
14
+ let preset;
15
+ let props;
16
+ let ref;
17
+ if ($[0] !== t0) {
18
+ ({children, preset, ref, ...props} = t0);
19
+ $[0] = t0;
20
+ $[1] = children;
21
+ $[2] = preset;
22
+ $[3] = props;
23
+ $[4] = ref;
24
+ } else {
25
+ children = $[1];
26
+ preset = $[2];
27
+ props = $[3];
28
+ ref = $[4];
29
+ }
30
+ const [element, attach] = useAttachedElement(ref);
31
+ const reduceMotion = usePrefersReducedMotion();
32
+ let t1;
33
+ let t2;
34
+ if ($[5] !== element || $[6] !== preset || $[7] !== reduceMotion) {
35
+ t1 = () => {
36
+ if (!element || reduceMotion) {
37
+ return;
38
+ }
39
+ return attachGlideIndicator(element, GLIDE_PRESETS[preset]);
40
+ };
41
+ t2 = [
42
+ element,
43
+ preset,
44
+ reduceMotion
45
+ ];
46
+ $[5] = element;
47
+ $[6] = preset;
48
+ $[7] = reduceMotion;
49
+ $[8] = t1;
50
+ $[9] = t2;
51
+ } else {
52
+ t1 = $[8];
53
+ t2 = $[9];
54
+ }
55
+ useLayoutEffect(t1, t2);
56
+ let t3;
57
+ if ($[10] !== attach || $[11] !== children || $[12] !== props) {
58
+ t3 = /* @__PURE__ */ _jsx(Slot.Root, {
59
+ ...props,
60
+ ref: attach,
61
+ children
62
+ });
63
+ $[10] = attach;
64
+ $[11] = children;
65
+ $[12] = props;
66
+ $[13] = t3;
67
+ } else {
68
+ t3 = $[13];
69
+ }
70
+ return t3;
71
+ }