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,17 +1,54 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import { cn } from "../../utils.js";
4
4
  /* empty css */
5
+ const TAIL_SEGMENTS = 18;
6
+ const BLOOM_SEGMENTS = 8;
7
+ function tailSegments(count) {
8
+ return Array.from({ length: count }, (_, i) => {
9
+ const t = (i + 0.5) / count;
10
+ return {
11
+ t: Number(t.toFixed(4)),
12
+ alpha: Number(Math.pow(1 - t, 1.6).toFixed(4)),
13
+ thickness: Number((1 - 0.7 * t).toFixed(4))
14
+ };
15
+ });
16
+ }
17
+ function clamp01(n) {
18
+ return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
19
+ }
20
+ function beamPhase(duration, delay, anchor, reverse) {
21
+ const d = duration > 0 ? duration : 1;
22
+ const lag = (delay % d + d) % d;
23
+ const offset = lag === 0 ? 0 : -(d - lag);
24
+ const dir = reverse ? -1 : 1;
25
+ const park = anchor - dir * lag * 100 / d;
26
+ return { offset: Number(offset.toFixed(4)), park: Number(park.toFixed(4)) };
27
+ }
28
+ const tailPieces = tailSegments(TAIL_SEGMENTS);
29
+ const bloomPieces = tailSegments(BLOOM_SEGMENTS);
30
+ function piece(p, count) {
31
+ return {
32
+ "--bb-t": p.t,
33
+ "--bb-alpha": p.alpha,
34
+ "--bb-thick": p.thickness,
35
+ "--bb-count": count
36
+ };
37
+ }
5
38
  function BorderBeam({
6
39
  className,
7
40
  size = 200,
8
- duration = 15,
41
+ duration = 9,
9
42
  borderWidth = 1.5,
10
43
  anchor = 90,
11
- colorFrom = "#ffaa40",
12
- colorTo = "#9c40ff",
13
- delay = 0
44
+ colorFrom = "#8ec5ff",
45
+ colorTo = "#c084fc",
46
+ delay = 0,
47
+ tail = 0.25,
48
+ glow = 0.6,
49
+ reverse = false
14
50
  }) {
51
+ const phase = beamPhase(duration, delay, anchor, reverse);
15
52
  const style = {
16
53
  "--border-beam-size": size,
17
54
  "--border-beam-duration": `${duration}s`,
@@ -19,27 +56,67 @@ function BorderBeam({
19
56
  "--border-beam-border-width": borderWidth,
20
57
  "--border-beam-color-from": colorFrom,
21
58
  "--border-beam-color-to": colorTo,
22
- "--border-beam-delay": `${delay}s`
59
+ "--border-beam-delay": `${delay}s`,
60
+ "--border-beam-tail": clamp01(tail),
61
+ "--border-beam-glow": clamp01(glow),
62
+ "--border-beam-offset": `${phase.offset}s`,
63
+ "--border-beam-park": `${phase.park}%`,
64
+ "--border-beam-dir": reverse ? -1 : 1
23
65
  };
24
- return /* @__PURE__ */ jsx(
66
+ return /* @__PURE__ */ jsxs(
25
67
  "div",
26
68
  {
27
69
  className: cn(
28
70
  "border-beam",
29
71
  "pointer-events-none absolute inset-0 rounded-[inherit]",
30
- "[border:calc(var(--border-beam-border-width)*1px)_solid_transparent]",
31
- "![mask-composite:intersect] ![mask-clip:padding-box,border-box] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
32
- "after:absolute after:aspect-square after:w-[calc(var(--border-beam-size)*1px)]",
33
- "after:[animation-delay:var(--border-beam-delay)]",
34
- "after:[background:linear-gradient(to_left,var(--border-beam-color-from),var(--border-beam-color-to),transparent)]",
35
- "after:[offset-anchor:calc(var(--border-beam-anchor)*1%)_50%]",
36
- "after:[offset-path:rect(0_auto_auto_0_round_calc(var(--border-beam-size)*1px))]",
37
72
  className
38
73
  ),
39
- style
74
+ "data-direction": reverse ? "reverse" : "forward",
75
+ "aria-hidden": "true",
76
+ style,
77
+ children: [
78
+ /* @__PURE__ */ jsx("div", { className: "bb-spill-clip", children: /* @__PURE__ */ jsx("span", { className: "bb-ride bb-spill" }) }),
79
+ /* @__PURE__ */ jsxs("div", { className: "bb-bloom", children: [
80
+ bloomPieces.map((p) => /* @__PURE__ */ jsx(
81
+ "span",
82
+ {
83
+ className: "bb-ride bb-piece bb-bloom-piece",
84
+ style: piece(p, BLOOM_SEGMENTS)
85
+ },
86
+ p.t
87
+ )),
88
+ /* @__PURE__ */ jsx("span", { className: "bb-ride bb-bloom-head" })
89
+ ] }),
90
+ /* @__PURE__ */ jsx("div", { className: "bb-halo", children: bloomPieces.map((p) => /* @__PURE__ */ jsx(
91
+ "span",
92
+ {
93
+ className: "bb-ride bb-piece bb-halo-piece",
94
+ style: piece(p, BLOOM_SEGMENTS)
95
+ },
96
+ p.t
97
+ )) }),
98
+ /* @__PURE__ */ jsxs("div", { className: "bb-ring", children: [
99
+ /* @__PURE__ */ jsx("span", { className: "bb-rail" }),
100
+ /* @__PURE__ */ jsx("span", { className: "bb-ride bb-reach" }),
101
+ tailPieces.map((p) => /* @__PURE__ */ jsx(
102
+ "span",
103
+ {
104
+ className: "bb-ride bb-piece bb-tail-piece",
105
+ style: piece(p, TAIL_SEGMENTS)
106
+ },
107
+ p.t
108
+ )),
109
+ /* @__PURE__ */ jsx("span", { className: "bb-ride bb-head" })
110
+ ] }),
111
+ /* @__PURE__ */ jsx("span", { className: "bb-ride bb-spark" })
112
+ ]
40
113
  }
41
114
  );
42
115
  }
43
116
  export {
44
- BorderBeam
117
+ BLOOM_SEGMENTS,
118
+ BorderBeam,
119
+ TAIL_SEGMENTS,
120
+ beamPhase,
121
+ tailSegments
45
122
  };
@@ -1,4 +1,26 @@
1
+ /**
2
+ * Compare - a before/after slider split by a blade of light
3
+ *
4
+ * The seam is a beam: a white-hot core with a chromatic fringe, a glow
5
+ * that spills onto both sides, and pulses of light running along it. Moving
6
+ * the seam leaves a trail behind it that stretches with the speed of the
7
+ * gesture and settles when it stops.
8
+ */
1
9
  import { type ReactNode } from "react";
10
+ export declare const COMPARE_BEAM_COLORS: string[];
11
+ /** Three colours for the beam (left fringe, centre, right fringe), from one to many given. */
12
+ export declare function beamPalette(colors: string[] | undefined): [string, string, string];
13
+ /**
14
+ * One frame of the beam's motion: the trail follows the seam's velocity
15
+ * (px per frame, signed) and decays; the energy eases toward its target
16
+ * and is kicked up by speed.
17
+ */
18
+ export declare function beamStep(trail: number, energy: number, deltaPx: number, target: number): {
19
+ trail: number;
20
+ energy: number;
21
+ };
22
+ /** Keyboard step for the slider, in %: arrows move 2, Shift+arrows 10, Home/End jump to the ends. */
23
+ export declare function keyStep(key: string, shift: boolean, current: number): number | null;
2
24
  export interface CompareProps {
3
25
  firstImage?: string;
4
26
  secondImage?: string;
@@ -12,6 +34,15 @@ export interface CompareProps {
12
34
  showHandlebar?: boolean;
13
35
  autoplay?: boolean;
14
36
  autoplayDuration?: number;
37
+ /** Beam colours: left fringe, centre, right fringe (one or more colours) */
38
+ beamColors?: string[];
39
+ /** Accessible name of the slider */
40
+ label?: string;
41
+ /**
42
+ * @deprecated Use `label`. Kept so code written against the earlier port
43
+ * keeps its accessible name; `label` wins when both are given.
44
+ */
45
+ ariaLabel?: string;
15
46
  onpercentagechange?: (percentage: number) => void;
16
47
  ondragstart?: () => void;
17
48
  ondragend?: () => void;
@@ -20,12 +51,5 @@ export interface CompareProps {
20
51
  firstContent?: ReactNode;
21
52
  secondContent?: ReactNode;
22
53
  handle?: ReactNode;
23
- /**
24
- * Accessible name for the slider. The element carries `role="slider"` and
25
- * sits in the tab order, so it needs one — the two images around it are not
26
- * its label. Overridable because "before and after" only says something when
27
- * the caller says what changed.
28
- */
29
- ariaLabel?: string;
30
54
  }
31
- export declare function Compare({ firstImage, secondImage, firstImageAlt, secondImageAlt, className, firstContentClass, secondContentClass, initialSliderPercentage, slideMode, showHandlebar, autoplay, autoplayDuration, onpercentagechange, ondragstart, ondragend, onhoverenter, onhoverleave, firstContent, secondContent, handle, ariaLabel, }: CompareProps): import("react").JSX.Element;
55
+ export declare function Compare({ firstImage, secondImage, firstImageAlt, secondImageAlt, className, firstContentClass, secondContentClass, initialSliderPercentage, slideMode, showHandlebar, autoplay, autoplayDuration, beamColors, label, ariaLabel, onpercentagechange, ondragstart, ondragend, onhoverenter, onhoverleave, firstContent, secondContent, handle, }: CompareProps): import("react").JSX.Element;
@@ -1,12 +1,46 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
- import { useRef, useState } from "react";
3
+ import { useRef, useState, useEffect } from "react";
4
4
  import { useIsomorphicLayoutEffect } from "../../internals/dom/ssr.js";
5
5
  import { useLiveRef } from "../../internals/dom/use-live-ref.js";
6
+ import { useReducedMotion } from "../../internals/motion/media-query.js";
6
7
  import { cn } from "../../utils.js";
7
- import { StarField } from "./StarField.js";
8
- const KEYBOARD_STEP = 1;
9
- const KEYBOARD_PAGE_STEP = 10;
8
+ /* empty css */
9
+ const COMPARE_BEAM_COLORS = ["#22d3ee", "#818cf8", "#f472b6"];
10
+ function beamPalette(colors) {
11
+ const list = (colors ?? []).filter(Boolean);
12
+ if (list.length === 0) return COMPARE_BEAM_COLORS;
13
+ if (list.length === 1) return [list[0], list[0], list[0]];
14
+ if (list.length === 2) return [list[0], `color-mix(in srgb, ${list[0]}, ${list[1]})`, list[1]];
15
+ return [list[0], list[Math.floor((list.length - 1) / 2)], list[list.length - 1]];
16
+ }
17
+ function beamStep(trail, energy, deltaPx, target) {
18
+ const nextTrail = Math.max(-140, Math.min(140, trail * 0.86 + deltaPx * 1.6));
19
+ const kick = Math.min(1, Math.abs(deltaPx) / 12);
20
+ const nextEnergy = Math.min(1, energy + (Math.max(target, kick) - energy) * 0.12);
21
+ return { trail: Math.abs(nextTrail) < 0.25 ? 0 : nextTrail, energy: nextEnergy };
22
+ }
23
+ function keyStep(key, shift, current) {
24
+ const step = shift ? 10 : 2;
25
+ switch (key) {
26
+ case "ArrowLeft":
27
+ case "ArrowDown":
28
+ return Math.max(0, current - step);
29
+ case "ArrowRight":
30
+ case "ArrowUp":
31
+ return Math.min(100, current + step);
32
+ case "PageDown":
33
+ return Math.max(0, current - 10);
34
+ case "PageUp":
35
+ return Math.min(100, current + 10);
36
+ case "Home":
37
+ return 0;
38
+ case "End":
39
+ return 100;
40
+ default:
41
+ return null;
42
+ }
43
+ }
10
44
  function Compare({
11
45
  firstImage = "",
12
46
  secondImage = "",
@@ -20,6 +54,9 @@ function Compare({
20
54
  showHandlebar = true,
21
55
  autoplay = false,
22
56
  autoplayDuration = 5e3,
57
+ beamColors,
58
+ label,
59
+ ariaLabel,
23
60
  onpercentagechange,
24
61
  ondragstart,
25
62
  ondragend,
@@ -27,28 +64,51 @@ function Compare({
27
64
  onhoverleave,
28
65
  firstContent,
29
66
  secondContent,
30
- handle,
31
- ariaLabel = "Image comparison slider"
67
+ handle
32
68
  }) {
69
+ const accessibleName = label ?? ariaLabel ?? "Comparison slider";
70
+ const reducedMotion = useReducedMotion();
71
+ const reducedMotionRef = useLiveRef(reducedMotion);
33
72
  const sliderRef = useRef(null);
34
- const [sliderXPercent, setSliderXPercent] = useState(initialSliderPercentage);
35
- const [isInteracting, setIsInteracting] = useState(false);
73
+ const [sliderXPercent, setSliderXPercentState] = useState(initialSliderPercentage);
74
+ const percentRef = useRef(initialSliderPercentage);
75
+ const [isDragging, setIsDraggingState] = useState(false);
36
76
  const isDraggingRef = useRef(false);
77
+ const [isMouseOver, setIsMouseOverState] = useState(false);
37
78
  const isMouseOverRef = useRef(false);
79
+ const [isInteracting, setIsInteractingState] = useState(false);
80
+ const isInteractingRef = useRef(false);
38
81
  const autoplayRAF = useRef(null);
82
+ const returnRAF = useRef(null);
39
83
  const moveRAF = useRef(null);
84
+ const [trail, setTrail] = useState(0);
85
+ const [energy, setEnergy] = useState(0);
86
+ const trailRef = useRef(0);
87
+ const energyRef = useRef(0);
88
+ const beamRAF = useRef(null);
89
+ const lastPx = useRef(null);
40
90
  const onPercentageChangeRef = useLiveRef(onpercentagechange);
41
- function stopAutoplay() {
42
- if (autoplayRAF.current) {
43
- cancelAnimationFrame(autoplayRAF.current);
44
- autoplayRAF.current = null;
45
- }
91
+ const palette = beamPalette(beamColors);
92
+ function setSliderX(p) {
93
+ percentRef.current = p;
94
+ setSliderXPercentState(p);
46
95
  }
47
- function stopMoveFrame() {
48
- if (moveRAF.current) {
49
- cancelAnimationFrame(moveRAF.current);
50
- moveRAF.current = null;
51
- }
96
+ function setIsDragging(v) {
97
+ isDraggingRef.current = v;
98
+ setIsDraggingState(v);
99
+ }
100
+ function setIsMouseOver(v) {
101
+ isMouseOverRef.current = v;
102
+ setIsMouseOverState(v);
103
+ }
104
+ function setIsInteracting(v) {
105
+ isInteractingRef.current = v;
106
+ setIsInteractingState(v);
107
+ }
108
+ function setPercent(p) {
109
+ const next = Math.max(0, Math.min(100, p));
110
+ setSliderX(next);
111
+ onPercentageChangeRef.current?.(next);
52
112
  }
53
113
  function startAutoplay() {
54
114
  if (!autoplay || isMouseOverRef.current || isDraggingRef.current) return;
@@ -60,38 +120,74 @@ function Compare({
60
120
  }
61
121
  const elapsedTime = Date.now() - startTime;
62
122
  const progress = elapsedTime % (autoplayDuration * 2) / autoplayDuration;
63
- const percentage = progress <= 1 ? progress * 100 : (2 - progress) * 100;
64
- setSliderXPercent(percentage);
65
- onPercentageChangeRef.current?.(percentage);
123
+ const linear = progress <= 1 ? progress : 2 - progress;
124
+ const eased = linear < 0.5 ? 4 * linear ** 3 : 1 - (-2 * linear + 2) ** 3 / 2;
125
+ setPercent(eased * 100);
66
126
  autoplayRAF.current = requestAnimationFrame(animate);
67
127
  }
68
128
  animate();
69
129
  }
130
+ function stopAutoplay() {
131
+ if (autoplayRAF.current) {
132
+ cancelAnimationFrame(autoplayRAF.current);
133
+ autoplayRAF.current = null;
134
+ }
135
+ }
136
+ function stopReturn() {
137
+ if (returnRAF.current) {
138
+ cancelAnimationFrame(returnRAF.current);
139
+ returnRAF.current = null;
140
+ }
141
+ }
142
+ function stopMoveFrame() {
143
+ if (moveRAF.current) {
144
+ cancelAnimationFrame(moveRAF.current);
145
+ moveRAF.current = null;
146
+ }
147
+ }
148
+ function glideTo(target) {
149
+ stopReturn();
150
+ if (reducedMotionRef.current) {
151
+ setPercent(target);
152
+ return;
153
+ }
154
+ const from = percentRef.current;
155
+ const start = performance.now();
156
+ const duration = 520;
157
+ const step = (now) => {
158
+ const k = Math.min(1, (now - start) / duration);
159
+ const e = 1 - (1 - k) ** 3;
160
+ setPercent(from + (target - from) * e);
161
+ returnRAF.current = k < 1 ? requestAnimationFrame(step) : null;
162
+ };
163
+ returnRAF.current = requestAnimationFrame(step);
164
+ }
70
165
  function mouseEnterHandler() {
71
- isMouseOverRef.current = true;
166
+ setIsMouseOver(true);
167
+ stopReturn();
72
168
  onhoverenter?.();
73
169
  if (autoplay) {
74
170
  stopAutoplay();
75
171
  }
76
172
  }
77
173
  function mouseLeaveHandler() {
78
- isMouseOverRef.current = false;
174
+ setIsMouseOver(false);
79
175
  setIsInteracting(false);
80
176
  onhoverleave?.();
81
177
  if (slideMode === "hover") {
82
- setSliderXPercent(initialSliderPercentage);
83
- onpercentagechange?.(initialSliderPercentage);
178
+ glideTo(initialSliderPercentage);
84
179
  }
85
180
  if (slideMode === "drag") {
86
- isDraggingRef.current = false;
181
+ setIsDragging(false);
87
182
  }
88
183
  if (autoplay) {
184
+ stopReturn();
89
185
  startAutoplay();
90
186
  }
91
187
  }
92
188
  function handleStart() {
93
189
  if (slideMode === "drag") {
94
- isDraggingRef.current = true;
190
+ setIsDragging(true);
95
191
  setIsInteracting(true);
96
192
  ondragstart?.();
97
193
  stopAutoplay();
@@ -99,7 +195,7 @@ function Compare({
99
195
  }
100
196
  function handleEnd() {
101
197
  if (slideMode === "drag") {
102
- isDraggingRef.current = false;
198
+ setIsDragging(false);
103
199
  setIsInteracting(false);
104
200
  ondragend?.();
105
201
  if (autoplay && !isMouseOverRef.current) {
@@ -107,56 +203,22 @@ function Compare({
107
203
  }
108
204
  }
109
205
  }
110
- function commitPercent(percent) {
111
- const newPercent = Math.max(0, Math.min(100, percent));
112
- setSliderXPercent(newPercent);
113
- onpercentagechange?.(newPercent);
114
- }
115
206
  function handleMove(clientX) {
116
207
  if (!sliderRef.current) return;
117
208
  if (slideMode === "hover" || slideMode === "drag" && isDraggingRef.current) {
118
209
  setIsInteracting(true);
119
210
  stopAutoplay();
211
+ stopReturn();
120
212
  const rect = sliderRef.current.getBoundingClientRect();
121
213
  const x = clientX - rect.left;
122
214
  const percent = x / rect.width * 100;
123
215
  stopMoveFrame();
124
216
  moveRAF.current = requestAnimationFrame(() => {
125
217
  moveRAF.current = null;
126
- commitPercent(percent);
218
+ setPercent(percent);
127
219
  });
128
220
  }
129
221
  }
130
- function handleKeyDown(event) {
131
- let next;
132
- switch (event.key) {
133
- case "ArrowLeft":
134
- case "ArrowDown":
135
- next = sliderXPercent - KEYBOARD_STEP;
136
- break;
137
- case "ArrowRight":
138
- case "ArrowUp":
139
- next = sliderXPercent + KEYBOARD_STEP;
140
- break;
141
- case "PageDown":
142
- next = sliderXPercent - KEYBOARD_PAGE_STEP;
143
- break;
144
- case "PageUp":
145
- next = sliderXPercent + KEYBOARD_PAGE_STEP;
146
- break;
147
- case "Home":
148
- next = 0;
149
- break;
150
- case "End":
151
- next = 100;
152
- break;
153
- default:
154
- return;
155
- }
156
- event.preventDefault();
157
- stopAutoplay();
158
- commitPercent(next);
159
- }
160
222
  function handleMouseDown() {
161
223
  handleStart();
162
224
  }
@@ -173,29 +235,73 @@ function Compare({
173
235
  const touch = e.touches[0];
174
236
  if (!autoplay && touch) handleMove(touch.clientX);
175
237
  }
238
+ function handleKeyDown(e) {
239
+ const next = keyStep(e.key, e.shiftKey, percentRef.current);
240
+ if (next === null) return;
241
+ e.preventDefault();
242
+ stopAutoplay();
243
+ stopReturn();
244
+ setPercent(next);
245
+ }
246
+ function wakeBeam() {
247
+ if (beamRAF.current !== null || reducedMotionRef.current) return;
248
+ const tick = () => {
249
+ const width = sliderRef.current?.clientWidth ?? 0;
250
+ const px = percentRef.current / 100 * width;
251
+ const delta = lastPx.current === null ? 0 : px - lastPx.current;
252
+ lastPx.current = px;
253
+ const target = isInteractingRef.current || isDraggingRef.current ? 1 : isMouseOverRef.current ? 0.7 : 0;
254
+ const next = beamStep(trailRef.current, energyRef.current, delta, target);
255
+ trailRef.current = next.trail;
256
+ energyRef.current = next.energy;
257
+ setTrail(next.trail);
258
+ setEnergy(next.energy);
259
+ const settled = next.trail === 0 && delta === 0 && Math.abs(next.energy - target) < 0.01;
260
+ beamRAF.current = settled ? null : requestAnimationFrame(tick);
261
+ };
262
+ beamRAF.current = requestAnimationFrame(tick);
263
+ }
176
264
  useIsomorphicLayoutEffect(() => {
177
- setSliderXPercent(initialSliderPercentage);
265
+ setSliderX(initialSliderPercentage);
178
266
  }, [initialSliderPercentage]);
267
+ useEffect(() => {
268
+ wakeBeam();
269
+ }, [sliderXPercent, isInteracting, isMouseOver]);
179
270
  useIsomorphicLayoutEffect(() => {
180
271
  if (autoplay && !isMouseOverRef.current && !isDraggingRef.current) {
181
272
  startAutoplay();
182
273
  } else {
183
274
  stopAutoplay();
184
275
  }
185
- return () => {
276
+ return stopAutoplay;
277
+ }, [autoplay, autoplayDuration]);
278
+ useEffect(
279
+ () => () => {
186
280
  stopAutoplay();
281
+ stopReturn();
187
282
  stopMoveFrame();
188
- };
189
- }, [autoplay, autoplayDuration]);
283
+ if (beamRAF.current !== null) cancelAnimationFrame(beamRAF.current);
284
+ beamRAF.current = null;
285
+ },
286
+ // eslint-disable-next-line react-hooks/exhaustive-deps
287
+ []
288
+ );
289
+ const rootStyle = {
290
+ position: "relative",
291
+ cursor: slideMode === "drag" ? isDragging ? "grabbing" : "grab" : "col-resize",
292
+ "--cmp-c1": palette[0],
293
+ "--cmp-c2": palette[1],
294
+ "--cmp-c3": palette[2],
295
+ "--cmp-energy": energy.toFixed(3),
296
+ "--cmp-trail": trail.toFixed(1)
297
+ };
190
298
  return /* @__PURE__ */ jsxs(
191
299
  "div",
192
300
  {
193
301
  ref: sliderRef,
194
- className: cn("h-[400px] w-[400px] overflow-hidden", className),
195
- style: {
196
- position: "relative",
197
- cursor: slideMode === "drag" ? "grab" : "col-resize"
198
- },
302
+ className: cn("compare h-[400px] w-[400px] overflow-hidden", className),
303
+ style: rootStyle,
304
+ "data-active": isInteracting || isDragging ? "" : void 0,
199
305
  onMouseMove: handleMouseMove,
200
306
  onMouseLeave: mouseLeaveHandler,
201
307
  onMouseEnter: mouseEnterHandler,
@@ -206,41 +312,49 @@ function Compare({
206
312
  onTouchMove: handleTouchMove,
207
313
  onKeyDown: handleKeyDown,
208
314
  role: "slider",
209
- "aria-label": ariaLabel,
210
- "aria-valuenow": sliderXPercent,
315
+ "aria-label": accessibleName,
316
+ "aria-valuenow": Math.round(sliderXPercent),
317
+ "aria-valuetext": `${Math.round(sliderXPercent)}% ${firstImageAlt}`,
211
318
  "aria-valuemin": 0,
212
319
  "aria-valuemax": 100,
320
+ "aria-orientation": "horizontal",
213
321
  tabIndex: 0,
214
322
  children: [
215
323
  /* @__PURE__ */ jsxs(
216
324
  "div",
217
325
  {
218
- className: "pointer-events-none absolute top-0 z-40 m-auto h-full w-px bg-gradient-to-b from-transparent from-5% via-indigo-500 to-transparent to-95%",
326
+ className: "compare-beam pointer-events-none absolute top-0 z-40 h-full w-px",
219
327
  style: { left: `${sliderXPercent}%` },
328
+ "aria-hidden": "true",
220
329
  children: [
221
- /* @__PURE__ */ jsx("div", { className: "absolute top-1/2 left-0 z-20 h-full w-36 -translate-y-1/2 bg-gradient-to-r from-indigo-400 via-transparent to-transparent [mask-image:radial-gradient(100px_at_left,white,transparent)] opacity-50" }),
222
- /* @__PURE__ */ jsx("div", { className: "absolute top-1/2 left-0 z-10 h-1/2 w-10 -translate-y-1/2 bg-gradient-to-r from-cyan-400 via-transparent to-transparent [mask-image:radial-gradient(50px_at_left,white,transparent)] opacity-100" }),
223
- /* @__PURE__ */ jsx("div", { className: "absolute top-1/2 -right-10 h-3/4 w-10 -translate-y-1/2 [mask-image:radial-gradient(100px_at_left,white,transparent)]", children: /* @__PURE__ */ jsx(StarField, { starsCount: 120, className: "size-full" }) }),
224
- handle ? handle : showHandlebar ? /* @__PURE__ */ jsx("div", { className: "pointer-events-auto absolute top-1/2 -right-2.5 z-30 flex size-5 -translate-y-1/2 cursor-grab items-center justify-center rounded-md bg-white shadow-[0px_-1px_0px_0px_#FFFFFF40]", children: /* @__PURE__ */ jsx(
330
+ /* @__PURE__ */ jsx("span", { className: "compare-beam__glow" }),
331
+ /* @__PURE__ */ jsx("span", { className: "compare-beam__trail" }),
332
+ /* @__PURE__ */ jsx("span", { className: "compare-beam__fringe" }),
333
+ /* @__PURE__ */ jsx("span", { className: "compare-beam__core" }),
334
+ /* @__PURE__ */ jsx("span", { className: "compare-beam__pulse" }),
335
+ /* @__PURE__ */ jsx("span", { className: "compare-beam__pulse compare-beam__pulse--late" })
336
+ ]
337
+ }
338
+ ),
339
+ /* @__PURE__ */ jsx(
340
+ "div",
341
+ {
342
+ className: "compare-seam pointer-events-none absolute top-0 z-[41] h-full w-px",
343
+ style: { left: `${sliderXPercent}%` },
344
+ children: handle ? handle : showHandlebar ? /* @__PURE__ */ jsxs("div", { className: "compare-handle pointer-events-auto", "aria-hidden": "true", children: [
345
+ /* @__PURE__ */ jsx("span", { className: "compare-handle__ring" }),
346
+ /* @__PURE__ */ jsx(
225
347
  "svg",
226
348
  {
227
- xmlns: "http://www.w3.org/2000/svg",
228
- fill: "none",
229
349
  viewBox: "0 0 24 24",
230
- strokeWidth: "1.5",
350
+ fill: "none",
231
351
  stroke: "currentColor",
232
- className: "size-4 text-black",
233
- children: /* @__PURE__ */ jsx(
234
- "path",
235
- {
236
- strokeLinecap: "round",
237
- strokeLinejoin: "round",
238
- d: "M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
239
- }
240
- )
352
+ strokeWidth: "2",
353
+ className: "size-4",
354
+ children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 7 4 12l5 5M15 7l5 5-5 5" })
241
355
  }
242
- ) }) : null
243
- ]
356
+ )
357
+ ] }) : null
244
358
  }
245
359
  ),
246
360
  /* @__PURE__ */ jsx(
@@ -296,5 +410,9 @@ function Compare({
296
410
  );
297
411
  }
298
412
  export {
299
- Compare
413
+ COMPARE_BEAM_COLORS,
414
+ Compare,
415
+ beamPalette,
416
+ beamStep,
417
+ keyStep
300
418
  };
@@ -1,10 +1,32 @@
1
1
  import type { ReactNode } from "react";
2
+ /**
3
+ * ContainerScroll - a scroll-driven aperture
4
+ *
5
+ * The card starts shut: a slim horizontal slit with a bright seam of light
6
+ * across its middle. As the section scrolls into view the slit opens
7
+ * vertically to the full card, the seam splits into two lips that ride the
8
+ * opening edges and fade, the content settles from a slight zoom into
9
+ * focus, and the title above blurs away to hand the stage to the card.
10
+ * No 3D tilt, no perspective.
11
+ */
2
12
  export interface ContainerScrollProps {
3
- /** Additional CSS classes */
13
+ /** Additional CSS classes on the section */
4
14
  className?: string;
5
- /** Content rendered in the title area above the card */
15
+ /** Title shown above the card (blurs and fades as the card opens) */
6
16
  titleContent?: ReactNode;
7
- /** Content rendered inside the tilting card */
17
+ /** Content revealed inside the card */
8
18
  cardContent?: ReactNode;
19
+ /** Seam colour (any CSS colour). Defaults to a soft blue. */
20
+ accent?: string;
21
+ /** Second seam tint, blended towards the seam ends. Defaults to a soft lilac. */
22
+ accentSecondary?: string;
9
23
  }
10
- export declare function ContainerScroll({ className, titleContent, cardContent, }: ContainerScrollProps): import("react").JSX.Element;
24
+ /**
25
+ * Raw scroll progress of the card track, measured at its centre (where the
26
+ * slit is): 0 while the slit is still in the bottom 8% of the viewport,
27
+ * 1 once it has risen to just above the middle of the viewport.
28
+ */
29
+ export declare function apertureProgress(top: number, height: number, viewport: number): number;
30
+ /** Smoothstep: a gentle start and a slow settle, so the opening reads as a lens, not a shutter. */
31
+ export declare function apertureEase(p: number): number;
32
+ export declare function ContainerScroll({ className, titleContent, cardContent, accent, accentSecondary, }: ContainerScrollProps): import("react").JSX.Element;