fancy-ui-react 0.3.0 → 0.4.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.
Files changed (86) hide show
  1. package/README.md +52 -19
  2. package/dist/components/animated-beam/AnimatedBeam.d.ts +47 -11
  3. package/dist/components/animated-beam/AnimatedBeam.js +293 -118
  4. package/dist/components/animated-tooltip/AnimatedTooltip.d.ts +10 -1
  5. package/dist/components/animated-tooltip/AnimatedTooltip.js +170 -101
  6. package/dist/components/bento-grid/BentoFrame.d.ts +16 -0
  7. package/dist/components/bento-grid/BentoFrame.js +28 -0
  8. package/dist/components/bento-grid/BentoGrid.d.ts +5 -1
  9. package/dist/components/bento-grid/BentoGrid.js +71 -2
  10. package/dist/components/bento-grid/BentoGridCard.js +34 -26
  11. package/dist/components/bento-grid/BentoGridItem.js +10 -10
  12. package/dist/components/border-beam/BorderBeam.d.ts +55 -10
  13. package/dist/components/border-beam/BorderBeam.js +93 -16
  14. package/dist/components/compare/Compare.d.ts +32 -8
  15. package/dist/components/compare/Compare.js +213 -95
  16. package/dist/components/container-scroll/ContainerScroll.d.ts +26 -4
  17. package/dist/components/container-scroll/ContainerScroll.js +88 -49
  18. package/dist/components/dock/Dock.d.ts +10 -7
  19. package/dist/components/dock/Dock.js +76 -31
  20. package/dist/components/dock/DockIcon.d.ts +14 -3
  21. package/dist/components/dock/DockIcon.js +23 -19
  22. package/dist/components/dock/DockSeparator.d.ts +4 -1
  23. package/dist/components/dock/DockSeparator.js +5 -2
  24. package/dist/components/dock/types.d.ts +13 -6
  25. package/dist/components/dropdown-menu/menu-shared.d.ts +11 -8
  26. package/dist/components/editorial-engine/engine.js +1 -14
  27. package/dist/components/fireworks-hdr/webgl2-renderer.js +0 -19
  28. package/dist/components/fireworks-hdr/webgpu-renderer.js +3 -22
  29. package/dist/components/flip-card/FlipCard.d.ts +42 -1
  30. package/dist/components/flip-card/FlipCard.js +178 -25
  31. package/dist/components/flip-card/index.d.ts +1 -1
  32. package/dist/components/fluid-cursor/FluidCursor.js +5 -4
  33. package/dist/components/fluid-cursor/dev.d.ts +1 -0
  34. package/dist/components/fluid-cursor/dev.js +8 -0
  35. package/dist/components/fluid-cursor/fluid-shared.d.ts +0 -2
  36. package/dist/components/fluid-cursor/fluid-shared.js +0 -2
  37. package/dist/components/fluid-cursor/webgpu-engine.js +10 -24
  38. package/dist/components/glow-border/GlowBorder.d.ts +41 -1
  39. package/dist/components/glow-border/GlowBorder.js +96 -16
  40. package/dist/components/glow-border/index.d.ts +2 -2
  41. package/dist/components/gradient-button/GradientButton.d.ts +10 -5
  42. package/dist/components/gradient-button/GradientButton.js +25 -18
  43. package/dist/components/image-trail-cursor/ImageTrailCursor.js +1 -0
  44. package/dist/components/image-trail-cursor/trail-variants.d.ts +37 -1
  45. package/dist/components/image-trail-cursor/trail-variants.js +331 -8
  46. package/dist/components/interactive-hover-button/InteractiveHoverButton.js +30 -35
  47. package/dist/components/marquee/Marquee.d.ts +7 -3
  48. package/dist/components/marquee/Marquee.js +134 -2
  49. package/dist/components/marquee/ReviewCard.d.ts +2 -1
  50. package/dist/components/marquee/ReviewCard.js +20 -9
  51. package/dist/components/meteors/Meteors.d.ts +32 -10
  52. package/dist/components/meteors/Meteors.js +53 -20
  53. package/dist/components/mosaic-glow/mosaic-glow-core.d.ts +98 -10
  54. package/dist/components/neon-border/NeonBorder.d.ts +24 -5
  55. package/dist/components/neon-border/NeonBorder.js +37 -7
  56. package/dist/components/pagination/Pagination.js +220 -82
  57. package/dist/components/rainbow-button/RainbowButton.js +13 -5
  58. package/dist/components/ripple-button/RippleButton.d.ts +20 -1
  59. package/dist/components/ripple-button/RippleButton.js +44 -21
  60. package/dist/components/shimmer-button/ShimmerButton.d.ts +5 -5
  61. package/dist/components/shimmer-button/ShimmerButton.js +17 -19
  62. package/dist/components/smooth-cursor/SmoothCursor.d.ts +13 -9
  63. package/dist/components/smooth-cursor/SmoothCursor.js +114 -163
  64. package/dist/components/smooth-cursor/smooth-cursor-core.d.ts +85 -0
  65. package/dist/components/smooth-cursor/smooth-cursor-core.js +327 -0
  66. package/dist/components/stepper/Step.js +32 -19
  67. package/dist/components/stepper/Stepper.js +18 -2
  68. package/dist/components/stepper/types.d.ts +13 -0
  69. package/dist/components/timeline/Timeline.d.ts +8 -1
  70. package/dist/components/timeline/Timeline.js +165 -76
  71. package/dist/index.js +2 -1
  72. package/dist/internals/markdown.d.ts +5 -0
  73. package/dist/internals/motion/haptics.d.ts +0 -4
  74. package/dist/internals/motion/presence-core.d.ts +59 -0
  75. package/dist/internals/motion/presence-core.js +162 -0
  76. package/dist/internals/motion/presence.d.ts +2 -4
  77. package/dist/internals/motion/presence.js +1 -156
  78. package/dist/internals/motion/raf.d.ts +1 -1
  79. package/dist/internals/motion/stagger.d.ts +3 -4
  80. package/dist/internals/motion/tokens.js +4 -0
  81. package/dist/internals/motion/transitions.d.ts +17 -17
  82. package/dist/internals/motion/types.d.ts +6 -7
  83. package/dist/internals/relative-time.d.ts +1 -1
  84. package/dist/styles.css +3470 -203
  85. package/package.json +18 -2
  86. package/dist/components/fireworks-hdr/webgpu-types.d.ts +0 -159
@@ -1,10 +1,15 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import { forwardRef, useState, useRef, useEffect } from "react";
4
4
  import { cn } from "../../utils.js";
5
5
  import { buildPageRange } from "./pagination-range.js";
6
6
  import { useSoundCue } from "../../sound/use-sound.js";
7
+ import { useComposedRefs } from "../../internals/dom/use-composed-refs.js";
8
+ import { useIsomorphicLayoutEffect } from "../../internals/dom/ssr.js";
9
+ import { useReducedMotion } from "../../internals/motion/media-query.js";
10
+ import { EASINGS, DURATIONS } from "../../internals/motion/tokens.js";
7
11
  /* empty css */
12
+ const SLIDE_MS = DURATIONS.base;
8
13
  const pageButtonBase = "inline-flex size-8 shrink-0 items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50";
9
14
  const navButtonBase = "inline-flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50";
10
15
  const Pagination = forwardRef(
@@ -32,12 +37,138 @@ const Pagination = forwardRef(
32
37
  const isFirst = safePage <= 1;
33
38
  const isLast = safePage >= safeCount;
34
39
  const [popArmed, setPopArmed] = useState(false);
40
+ const armedRef = useRef(false);
35
41
  const lastPageRef = useRef(safePage);
42
+ const reduced = useReducedMotion();
43
+ const flipDuration = reduced ? 0 : SLIDE_MS;
44
+ const navRef = useRef(null);
45
+ const composedRef = useComposedRefs(navRef, forwardedRef);
46
+ const indicatorRef = useRef(null);
47
+ const [indicatorPlaced, setIndicatorPlaced] = useState(false);
48
+ function placeIndicator(animate) {
49
+ const el = indicatorRef.current;
50
+ const nav = navRef.current;
51
+ const current = nav?.querySelector('button[aria-current="page"]');
52
+ if (!el || !current || current.offsetWidth === 0) {
53
+ setIndicatorPlaced(false);
54
+ return;
55
+ }
56
+ let x = 0;
57
+ let y = 0;
58
+ for (let n = current; n && n !== nav; ) {
59
+ x += n.offsetLeft;
60
+ y += n.offsetTop;
61
+ n = n.offsetParent;
62
+ }
63
+ const transform = `translate(${x}px, ${y}px)`;
64
+ el.style.width = `${current.offsetWidth}px`;
65
+ el.style.height = `${current.offsetHeight}px`;
66
+ if (el.style.transform === transform) {
67
+ setIndicatorPlaced(true);
68
+ return;
69
+ }
70
+ if (animate && el.style.transform) {
71
+ el.style.transform = transform;
72
+ } else {
73
+ const previous = el.style.transition;
74
+ el.style.transition = "none";
75
+ el.style.transform = transform;
76
+ void el.offsetWidth;
77
+ el.style.transition = previous;
78
+ }
79
+ setIndicatorPlaced(true);
80
+ }
81
+ const itemEls = useRef(/* @__PURE__ */ new Map());
82
+ const itemRects = useRef(/* @__PURE__ */ new Map());
83
+ const itemAnims = useRef(/* @__PURE__ */ new Map());
84
+ const mountedRef = useRef(false);
85
+ function measureItems() {
86
+ const nav = navRef.current;
87
+ const origin = nav?.getBoundingClientRect();
88
+ const rects = /* @__PURE__ */ new Map();
89
+ for (const [key, el] of itemEls.current) {
90
+ const rect = el.getBoundingClientRect();
91
+ rects.set(key, {
92
+ left: rect.left - (origin?.left ?? 0),
93
+ top: rect.top - (origin?.top ?? 0)
94
+ });
95
+ }
96
+ return rects;
97
+ }
98
+ const itemsKey = items.join(",");
99
+ useIsomorphicLayoutEffect(() => {
100
+ const nav = navRef.current;
101
+ if (safePage !== lastPageRef.current) {
102
+ lastPageRef.current = safePage;
103
+ armedRef.current = true;
104
+ nav?.setAttribute("data-armed", "true");
105
+ setPopArmed(true);
106
+ }
107
+ const firstCommit = !mountedRef.current;
108
+ mountedRef.current = true;
109
+ const origin = nav?.getBoundingClientRect();
110
+ const nextRects = /* @__PURE__ */ new Map();
111
+ for (const [key, el] of itemEls.current) {
112
+ const running = itemAnims.current.get(key);
113
+ let driftX = 0;
114
+ let driftY = 0;
115
+ if (running) {
116
+ const live = el.getBoundingClientRect();
117
+ running.cancel();
118
+ const settled = el.getBoundingClientRect();
119
+ driftX = live.left - settled.left;
120
+ driftY = live.top - settled.top;
121
+ }
122
+ itemAnims.current.delete(key);
123
+ const rect = el.getBoundingClientRect();
124
+ const left = rect.left - (origin?.left ?? 0);
125
+ const top = rect.top - (origin?.top ?? 0);
126
+ nextRects.set(key, { left, top });
127
+ if (firstCommit || !flipDuration || typeof el.animate !== "function") continue;
128
+ const prev = itemRects.current.get(key);
129
+ if (prev) {
130
+ const dx = prev.left + driftX - left;
131
+ const dy = prev.top + driftY - top;
132
+ if (!dx && !dy) continue;
133
+ itemAnims.current.set(
134
+ key,
135
+ el.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "none" }], {
136
+ duration: flipDuration,
137
+ easing: EASINGS.out
138
+ })
139
+ );
140
+ } else {
141
+ itemAnims.current.set(
142
+ key,
143
+ el.animate([{ opacity: 0 }, { opacity: 1 }], {
144
+ duration: DURATIONS.fast,
145
+ delay: 60,
146
+ easing: "linear",
147
+ fill: "backwards"
148
+ })
149
+ );
150
+ }
151
+ }
152
+ itemRects.current = nextRects;
153
+ placeIndicator(armedRef.current);
154
+ }, [safePage, itemsKey, flipDuration]);
36
155
  useEffect(() => {
37
- if (safePage === lastPageRef.current) return;
38
- lastPageRef.current = safePage;
39
- setPopArmed(true);
40
- }, [safePage]);
156
+ const nav = navRef.current;
157
+ if (!nav || typeof ResizeObserver === "undefined") return;
158
+ const ro = new ResizeObserver(() => {
159
+ const gliding = [...itemAnims.current.values()].some((a) => a.playState === "running");
160
+ if (!gliding) itemRects.current = measureItems();
161
+ placeIndicator(false);
162
+ });
163
+ ro.observe(nav);
164
+ return () => ro.disconnect();
165
+ }, []);
166
+ function registerItem(key) {
167
+ return (el) => {
168
+ if (el) itemEls.current.set(key, el);
169
+ else itemEls.current.delete(key);
170
+ };
171
+ }
41
172
  function goTo(next) {
42
173
  if (disabled) return;
43
174
  const clamped = Math.max(1, Math.min(Math.floor(next), Math.max(safeCount, 1)));
@@ -62,90 +193,97 @@ const Pagination = forwardRef(
62
193
  if (disabled || isLast) return;
63
194
  goTo(safeCount);
64
195
  }
65
- return /* @__PURE__ */ jsx(
196
+ return /* @__PURE__ */ jsxs(
66
197
  "nav",
67
198
  {
68
- ref: forwardedRef,
199
+ ref: composedRef,
69
200
  "aria-label": label,
70
201
  "data-armed": popArmed ? "true" : void 0,
202
+ "data-indicator": indicatorPlaced ? "" : void 0,
71
203
  className: cn("ft-pagination", className),
72
- children: /* @__PURE__ */ jsxs("ul", { className: "flex items-center gap-1", children: [
73
- showEdges && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
74
- "button",
75
- {
76
- type: "button",
77
- className: navButtonBase,
78
- disabled: disabled || isFirst,
79
- "aria-label": "First page",
80
- title: "First page",
81
- onClick: handleFirst,
82
- children: "« First"
83
- }
84
- ) }),
85
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
86
- "button",
87
- {
88
- type: "button",
89
- className: navButtonBase,
90
- disabled: disabled || isFirst,
91
- "aria-label": "Previous page",
92
- title: "Previous page",
93
- onClick: handlePrevious,
94
- children: previousLabel ?? "‹ Previous"
95
- }
96
- ) }),
97
- items.map((item, i) => /* @__PURE__ */ jsx("li", { children: item === "ellipsis" ? (
98
- // Decorative only: it stands for a run of hidden pages, not a
99
- // control, so it must not take focus or be reachable by Tab.
100
- /* @__PURE__ */ jsx(
101
- "span",
204
+ children: [
205
+ /* @__PURE__ */ jsxs("ul", { className: "flex items-center gap-1", children: [
206
+ showEdges && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
207
+ "button",
208
+ {
209
+ type: "button",
210
+ className: navButtonBase,
211
+ disabled: disabled || isFirst,
212
+ "aria-label": "First page",
213
+ title: "First page",
214
+ onClick: handleFirst,
215
+ children: "« First"
216
+ }
217
+ ) }),
218
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
219
+ "button",
220
+ {
221
+ type: "button",
222
+ className: navButtonBase,
223
+ disabled: disabled || isFirst,
224
+ "aria-label": "Previous page",
225
+ title: "Previous page",
226
+ onClick: handlePrevious,
227
+ children: previousLabel ?? "‹ Previous"
228
+ }
229
+ ) }),
230
+ items.map((item, i) => {
231
+ const key = item === "ellipsis" ? `ellipsis-${i}` : `page-${item}`;
232
+ return /* @__PURE__ */ jsx("li", { ref: registerItem(key), children: item === "ellipsis" ? (
233
+ // Decorative only: it stands for a run of hidden pages, not a
234
+ // control, so it must not take focus or be reachable by Tab.
235
+ /* @__PURE__ */ jsx(
236
+ "span",
237
+ {
238
+ "aria-hidden": "true",
239
+ className: "text-muted-foreground flex size-8 items-center justify-center select-none",
240
+ children: "…"
241
+ }
242
+ )
243
+ ) : /* @__PURE__ */ jsx(
244
+ "button",
245
+ {
246
+ type: "button",
247
+ className: cn(
248
+ pageButtonBase,
249
+ item === safePage ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
250
+ ),
251
+ disabled,
252
+ "aria-current": item === safePage ? "page" : void 0,
253
+ "aria-label": `Go to page ${item}`,
254
+ title: `Go to page ${item}`,
255
+ onClick: () => goTo(item),
256
+ children: item
257
+ }
258
+ ) }, key);
259
+ }),
260
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
261
+ "button",
262
+ {
263
+ type: "button",
264
+ className: navButtonBase,
265
+ disabled: disabled || isLast,
266
+ "aria-label": "Next page",
267
+ title: "Next page",
268
+ onClick: handleNext,
269
+ children: nextLabel ?? "Next ›"
270
+ }
271
+ ) }),
272
+ showEdges && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
273
+ "button",
102
274
  {
103
- "aria-hidden": "true",
104
- className: "text-muted-foreground flex size-8 items-center justify-center select-none",
105
- children: "…"
275
+ type: "button",
276
+ className: navButtonBase,
277
+ disabled: disabled || isLast,
278
+ "aria-label": "Last page",
279
+ title: "Last page",
280
+ onClick: handleLast,
281
+ children: "Last »"
106
282
  }
107
- )
108
- ) : /* @__PURE__ */ jsx(
109
- "button",
110
- {
111
- type: "button",
112
- className: cn(
113
- pageButtonBase,
114
- item === safePage ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
115
- ),
116
- disabled,
117
- "aria-current": item === safePage ? "page" : void 0,
118
- "aria-label": `Go to page ${item}`,
119
- title: `Go to page ${item}`,
120
- onClick: () => goTo(item),
121
- children: item
122
- }
123
- ) }, item === "ellipsis" ? `ellipsis-${i}` : `page-${item}`)),
124
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
125
- "button",
126
- {
127
- type: "button",
128
- className: navButtonBase,
129
- disabled: disabled || isLast,
130
- "aria-label": "Next page",
131
- title: "Next page",
132
- onClick: handleNext,
133
- children: nextLabel ?? "Next ›"
134
- }
135
- ) }),
136
- showEdges && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
137
- "button",
138
- {
139
- type: "button",
140
- className: navButtonBase,
141
- disabled: disabled || isLast,
142
- "aria-label": "Last page",
143
- title: "Last page",
144
- onClick: handleLast,
145
- children: "Last »"
146
- }
147
- ) })
148
- ] })
283
+ ) })
284
+ ] }),
285
+ /* @__PURE__ */ jsx("span", { ref: indicatorRef, className: "ft-pagination-indicator", "aria-hidden": "true" })
286
+ ]
149
287
  }
150
288
  );
151
289
  }
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import { forwardRef } from "react";
4
4
  import { cn } from "../../utils.js";
5
5
  import { useSoundCue } from "../../sound/use-sound.js";
@@ -24,7 +24,7 @@ const RainbowButton = forwardRef(
24
24
  className
25
25
  );
26
26
  if (href) {
27
- return /* @__PURE__ */ jsx(
27
+ return /* @__PURE__ */ jsxs(
28
28
  "a",
29
29
  {
30
30
  ref,
@@ -35,11 +35,15 @@ const RainbowButton = forwardRef(
35
35
  role: disabled ? "link" : void 0,
36
36
  tabIndex: disabled ? -1 : void 0,
37
37
  onClick: handleClick,
38
- children
38
+ children: [
39
+ /* @__PURE__ */ jsx("span", { className: "rainbow-button__glow", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", {}) }),
40
+ /* @__PURE__ */ jsx("span", { className: "rainbow-button__beam", "aria-hidden": "true" }),
41
+ children
42
+ ]
39
43
  }
40
44
  );
41
45
  }
42
- return /* @__PURE__ */ jsx(
46
+ return /* @__PURE__ */ jsxs(
43
47
  "button",
44
48
  {
45
49
  ref,
@@ -48,7 +52,11 @@ const RainbowButton = forwardRef(
48
52
  type,
49
53
  disabled,
50
54
  onClick: handleClick,
51
- children
55
+ children: [
56
+ /* @__PURE__ */ jsx("span", { className: "rainbow-button__glow", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", {}) }),
57
+ /* @__PURE__ */ jsx("span", { className: "rainbow-button__beam", "aria-hidden": "true" }),
58
+ children
59
+ ]
52
60
  }
53
61
  );
54
62
  }
@@ -2,7 +2,7 @@ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
2
  export interface RippleButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className"> {
3
3
  /** Additional CSS classes */
4
4
  className?: string;
5
- /** Color of the ripple effect */
5
+ /** Color of the ripple: its glow, its rings and the border tint */
6
6
  rippleColor?: string;
7
7
  /** Animation duration in milliseconds */
8
8
  duration?: number;
@@ -14,4 +14,23 @@ export interface RippleButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonE
14
14
  */
15
15
  sound?: boolean;
16
16
  }
17
+ /**
18
+ * Where a ripple starts and how big it grows: from the pointer, or from the
19
+ * centre for a keyboard press (no pointer position), with a diameter that
20
+ * reaches the farthest corner.
21
+ */
22
+ export declare function rippleGeometry(rect: {
23
+ left: number;
24
+ top: number;
25
+ width: number;
26
+ height: number;
27
+ }, event: {
28
+ clientX: number;
29
+ clientY: number;
30
+ detail: number;
31
+ }): {
32
+ x: number;
33
+ y: number;
34
+ size: number;
35
+ };
17
36
  export declare const RippleButton: import("react").ForwardRefExoticComponent<RippleButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -5,63 +5,85 @@ import { useComposedRefs } from "../../internals/dom/use-composed-refs.js";
5
5
  import { useSoundCue } from "../../sound/use-sound.js";
6
6
  import { cn } from "../../utils.js";
7
7
  /* empty css */
8
+ function rippleGeometry(rect, event) {
9
+ const keyboard = event.detail === 0;
10
+ const cx = keyboard ? rect.width / 2 : event.clientX - rect.left;
11
+ const cy = keyboard ? rect.height / 2 : event.clientY - rect.top;
12
+ const dx = Math.max(cx, rect.width - cx);
13
+ const dy = Math.max(cy, rect.height - cy);
14
+ const size = 2 * Math.hypot(dx, dy);
15
+ return { x: cx - size / 2, y: cy - size / 2, size };
16
+ }
8
17
  const RippleButton = forwardRef(
9
18
  ({
10
19
  className,
11
- rippleColor = "#ADD8E6",
12
- duration = 600,
20
+ rippleColor = "#60a5fa",
21
+ duration = 900,
13
22
  children,
14
23
  onClick,
24
+ onPointerMove,
15
25
  sound = false,
16
26
  ...restProps
17
27
  }, forwardedRef) => {
18
28
  const buttonRef = useRef(null);
19
29
  const composedRef = useComposedRefs(forwardedRef, buttonRef);
20
30
  const [ripples, setRipples] = useState([]);
21
- const nextKey = useRef(0);
31
+ const nextRippleKey = useRef(0);
22
32
  const playCue = useSoundCue(sound);
33
+ function handleClick(event) {
34
+ if (!restProps.disabled) playCue("press");
35
+ createRipple(event);
36
+ onClick?.(event);
37
+ }
38
+ function handlePointerMove(event) {
39
+ const el = event.currentTarget;
40
+ const rect = el.getBoundingClientRect();
41
+ el.style.setProperty("--ripple-x", `${event.clientX - rect.left}px`);
42
+ el.style.setProperty("--ripple-y", `${event.clientY - rect.top}px`);
43
+ onPointerMove?.(event);
44
+ }
23
45
  function createRipple(event) {
24
46
  const button = buttonRef.current;
25
47
  if (!button) return;
26
- const rect = button.getBoundingClientRect();
27
- const size = Math.max(rect.width, rect.height);
28
- const x = event.clientX - rect.left - size / 2;
29
- const y = event.clientY - rect.top - size / 2;
30
- const newRipple = { x, y, size, key: nextKey.current++ };
48
+ const { x, y, size } = rippleGeometry(button.getBoundingClientRect(), event);
49
+ const newRipple = { x, y, size, key: nextRippleKey.current++ };
31
50
  setRipples((prev) => [...prev, newRipple]);
32
51
  setTimeout(() => {
33
52
  setRipples((prev) => prev.filter((r) => r.key !== newRipple.key));
34
53
  }, duration);
35
54
  }
36
- function handleClick(event) {
37
- if (!restProps.disabled) playCue("press");
38
- createRipple(event);
39
- onClick?.(event);
40
- }
41
55
  return /* @__PURE__ */ jsxs(
42
56
  "button",
43
57
  {
44
58
  ref: composedRef,
45
59
  className: cn(
46
- "ripple-button relative flex cursor-pointer items-center justify-center overflow-hidden",
47
- "bg-background text-primary rounded-lg border-2 px-4 py-2 text-center",
60
+ "ripple-button relative isolate flex cursor-pointer items-center justify-center overflow-hidden",
61
+ "bg-background text-foreground border-border rounded-lg border px-5 py-2.5 text-center font-medium",
62
+ "shadow-[inset_0_1px_0_rgb(255_255_255/0.06),0_1px_2px_rgb(0_0_0/0.08)]",
63
+ "transition-[transform,border-color,background-color] duration-300 ease-out",
64
+ "hover:bg-muted/40 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50",
48
65
  className
49
66
  ),
50
- style: { "--ripple-duration": `${duration}ms` },
67
+ style: {
68
+ "--ripple-duration": `${duration}ms`,
69
+ "--ripple-color": rippleColor
70
+ },
71
+ "data-rippling": ripples.length > 0 ? "" : void 0,
51
72
  onClick: handleClick,
73
+ onPointerMove: handlePointerMove,
52
74
  ...restProps,
53
75
  children: [
54
76
  /* @__PURE__ */ jsx("div", { className: "relative z-10", children }),
55
- /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute inset-0", children: ripples.map((ripple) => /* @__PURE__ */ jsx(
77
+ /* @__PURE__ */ jsx("span", { className: "ripple-hover", "aria-hidden": "true" }),
78
+ /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute inset-0", "aria-hidden": "true", children: ripples.map((ripple) => /* @__PURE__ */ jsx(
56
79
  "span",
57
80
  {
58
- className: "ripple-animation absolute rounded-full opacity-30",
81
+ className: "ripple-animation absolute rounded-full",
59
82
  style: {
60
83
  width: `${ripple.size}px`,
61
84
  height: `${ripple.size}px`,
62
85
  top: `${ripple.y}px`,
63
- left: `${ripple.x}px`,
64
- backgroundColor: rippleColor
86
+ left: `${ripple.x}px`
65
87
  }
66
88
  },
67
89
  ripple.key
@@ -73,5 +95,6 @@ const RippleButton = forwardRef(
73
95
  );
74
96
  RippleButton.displayName = "RippleButton";
75
97
  export {
76
- RippleButton
98
+ RippleButton,
99
+ rippleGeometry
77
100
  };
@@ -1,12 +1,12 @@
1
1
  import type { ButtonHTMLAttributes, ReactNode } from "react";
2
2
  type BaseProps = {
3
- /** Shimmer highlight color */
3
+ /** Colour of the sheen */
4
4
  shimmerColor?: string;
5
- /** Thickness of the shimmer border */
5
+ /** Thickness of the rim the sheen glints on */
6
6
  shimmerSize?: string;
7
7
  /** Button border radius */
8
8
  borderRadius?: string;
9
- /** Duration of the shimmer animation cycle */
9
+ /** Duration of one sheen cycle: the sweep, then a pause */
10
10
  shimmerDuration?: string;
11
11
  /** Button background color */
12
12
  background?: string;
@@ -15,8 +15,8 @@ type BaseProps = {
15
15
  /** Button content */
16
16
  children?: ReactNode;
17
17
  /**
18
- * Plays the matching interface cue through the sound controller. Off
19
- * by default; only audible once the user has enabled sound.
18
+ * Plays the matching interface cue through the sound controller. Off by
19
+ * default; only audible once the user has enabled sound.
20
20
  */
21
21
  sound?: boolean;
22
22
  };
@@ -13,12 +13,12 @@ const ShimmerButton = forwardRef(
13
13
  shimmerDuration = "3s",
14
14
  background = "rgba(0, 0, 0, 1)",
15
15
  children,
16
- sound = false,
17
16
  onClick,
17
+ onPointerMove,
18
+ sound = false,
18
19
  ...restProps
19
20
  }, ref) => {
20
21
  const styleVars = {
21
- "--spread": "90deg",
22
22
  "--shimmer-color": shimmerColor,
23
23
  "--radius": borderRadius,
24
24
  "--speed": shimmerDuration,
@@ -30,34 +30,32 @@ const ShimmerButton = forwardRef(
30
30
  if (sound && !restProps.disabled) playCue("press");
31
31
  onClick?.(event);
32
32
  }
33
+ function handlePointerMove(event) {
34
+ const el = event.currentTarget;
35
+ const rect = el.getBoundingClientRect();
36
+ el.style.setProperty("--mx", `${event.clientX - rect.left}px`);
37
+ el.style.setProperty("--my", `${event.clientY - rect.top}px`);
38
+ onPointerMove?.(event);
39
+ }
33
40
  return /* @__PURE__ */ jsxs(
34
41
  "button",
35
42
  {
36
43
  ref,
37
44
  className: cn(
38
- "shimmer-button group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden [border-radius:var(--radius)] border border-white/10 px-6 py-3 whitespace-nowrap text-white [background:var(--bg)]",
39
- "transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px",
45
+ "shimmer-button group relative isolate flex cursor-pointer items-center justify-center overflow-hidden [border-radius:var(--radius)] px-6 py-3 whitespace-nowrap text-white/90",
46
+ "transform-gpu transition-[transform,color] duration-300 ease-out hover:text-white active:scale-[0.98]",
40
47
  className
41
48
  ),
42
49
  style: styleVars,
43
50
  onClick: handleClick,
51
+ onPointerMove: handlePointerMove,
44
52
  ...restProps,
45
53
  children: [
46
- /* @__PURE__ */ jsx("div", { className: "[container-type:size] absolute inset-0 -z-30 overflow-visible blur-[2px]", children: /* @__PURE__ */ jsx("div", { className: "shimmer-slide absolute inset-0 [aspect-ratio:1] h-[100cqh] [border-radius:0] [mask:none]", children: /* @__PURE__ */ jsx("div", { className: "spin-around absolute -inset-full w-auto [translate:0_0] rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]" }) }) }),
47
- children,
48
- /* @__PURE__ */ jsx(
49
- "div",
50
- {
51
- className: cn(
52
- "insert-0 absolute size-full",
53
- "rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]",
54
- "transform-gpu transition-all duration-300 ease-in-out",
55
- "group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]",
56
- "group-active:shadow-[inset_0_-10px_10px_#ffffff3f]"
57
- )
58
- }
59
- ),
60
- /* @__PURE__ */ jsx("div", { className: "absolute [inset:var(--cut)] -z-20 [border-radius:var(--radius)] [background:var(--bg)]" })
54
+ /* @__PURE__ */ jsx("span", { className: "shimmer-button__rim", "aria-hidden": "true" }),
55
+ /* @__PURE__ */ jsx("span", { className: "shimmer-button__face", "aria-hidden": "true" }),
56
+ /* @__PURE__ */ jsx("span", { className: "relative z-10", children }),
57
+ /* @__PURE__ */ jsx("span", { className: "shimmer-button__sheen", "aria-hidden": "true" }),
58
+ /* @__PURE__ */ jsx("span", { className: "shimmer-button__spot", "aria-hidden": "true" })
61
59
  ]
62
60
  }
63
61
  );
@@ -1,12 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
- export interface SpringConfig {
3
- /** Controls how quickly the animation settles (default: 45) */
4
- damping?: number;
5
- /** Controls the spring stiffness (default: 400) */
6
- stiffness?: number;
7
- /** Controls the virtual mass of the animated object (default: 1) */
8
- mass?: number;
9
- }
2
+ import { type SpringConfig } from "./smooth-cursor-core.js";
3
+ export type { SpringConfig };
10
4
  export interface SmoothCursorProps {
11
5
  /** Custom cursor node to replace the default arrow cursor */
12
6
  cursor?: ReactNode;
@@ -14,5 +8,15 @@ export interface SmoothCursorProps {
14
8
  springConfig?: SpringConfig;
15
9
  /** Additional CSS classes */
16
10
  className?: string;
11
+ /** Name shown in a pill that trails the arrow. No pill when omitted. */
12
+ label?: string;
13
+ /** Fill colour of the arrow and the name pill (any CSS colour) */
14
+ color?: string;
15
+ /** Rotate the cursor toward its direction of travel (the arrow stays upright when false) */
16
+ rotate?: boolean;
17
+ /** Milliseconds without movement before the name pill dims; 0 disables */
18
+ idleFade?: number;
19
+ /** Spring for the name pill; missing fields derive from `springConfig` (stiffness × 0.55, damping × 1.1) */
20
+ labelSpring?: SpringConfig;
17
21
  }
18
- export declare function SmoothCursor({ cursor, springConfig, className }: SmoothCursorProps): import("react").JSX.Element;
22
+ export declare function SmoothCursor({ cursor, springConfig, className, label, color, rotate, idleFade, labelSpring, }: SmoothCursorProps): import("react").JSX.Element;