fancy-ui-react 0.2.1 → 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 (93) hide show
  1. package/README.md +56 -20
  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/button/Button.js +5 -1
  15. package/dist/components/compare/Compare.d.ts +32 -8
  16. package/dist/components/compare/Compare.js +213 -95
  17. package/dist/components/container-scroll/ContainerScroll.d.ts +26 -4
  18. package/dist/components/container-scroll/ContainerScroll.js +88 -49
  19. package/dist/components/datamosh-transition/DatamoshTransition.d.ts +76 -0
  20. package/dist/components/datamosh-transition/DatamoshTransition.js +368 -0
  21. package/dist/components/datamosh-transition/datamosh-transition-core.d.ts +180 -0
  22. package/dist/components/datamosh-transition/datamosh-transition-core.js +370 -0
  23. package/dist/components/datamosh-transition/index.d.ts +3 -0
  24. package/dist/components/dock/Dock.d.ts +10 -7
  25. package/dist/components/dock/Dock.js +76 -31
  26. package/dist/components/dock/DockIcon.d.ts +14 -3
  27. package/dist/components/dock/DockIcon.js +23 -19
  28. package/dist/components/dock/DockSeparator.d.ts +4 -1
  29. package/dist/components/dock/DockSeparator.js +5 -2
  30. package/dist/components/dock/types.d.ts +13 -6
  31. package/dist/components/dropdown-menu/menu-shared.d.ts +11 -8
  32. package/dist/components/editorial-engine/engine.js +1 -14
  33. package/dist/components/fireworks-hdr/webgl2-renderer.js +0 -19
  34. package/dist/components/fireworks-hdr/webgpu-renderer.js +3 -22
  35. package/dist/components/flip-card/FlipCard.d.ts +42 -1
  36. package/dist/components/flip-card/FlipCard.js +178 -25
  37. package/dist/components/flip-card/index.d.ts +1 -1
  38. package/dist/components/fluid-cursor/FluidCursor.js +5 -4
  39. package/dist/components/fluid-cursor/dev.d.ts +1 -0
  40. package/dist/components/fluid-cursor/dev.js +8 -0
  41. package/dist/components/fluid-cursor/fluid-shared.d.ts +0 -2
  42. package/dist/components/fluid-cursor/fluid-shared.js +0 -2
  43. package/dist/components/fluid-cursor/webgpu-engine.js +10 -24
  44. package/dist/components/glow-border/GlowBorder.d.ts +41 -1
  45. package/dist/components/glow-border/GlowBorder.js +96 -16
  46. package/dist/components/glow-border/index.d.ts +2 -2
  47. package/dist/components/gradient-button/GradientButton.d.ts +10 -5
  48. package/dist/components/gradient-button/GradientButton.js +25 -18
  49. package/dist/components/image-trail-cursor/ImageTrailCursor.js +1 -0
  50. package/dist/components/image-trail-cursor/trail-variants.d.ts +37 -1
  51. package/dist/components/image-trail-cursor/trail-variants.js +331 -8
  52. package/dist/components/interactive-hover-button/InteractiveHoverButton.js +30 -35
  53. package/dist/components/marquee/Marquee.d.ts +7 -3
  54. package/dist/components/marquee/Marquee.js +134 -2
  55. package/dist/components/marquee/ReviewCard.d.ts +2 -1
  56. package/dist/components/marquee/ReviewCard.js +20 -9
  57. package/dist/components/meteors/Meteors.d.ts +32 -10
  58. package/dist/components/meteors/Meteors.js +53 -20
  59. package/dist/components/mosaic-glow/mosaic-glow-core.d.ts +98 -10
  60. package/dist/components/neon-border/NeonBorder.d.ts +24 -5
  61. package/dist/components/neon-border/NeonBorder.js +37 -7
  62. package/dist/components/pagination/Pagination.js +220 -82
  63. package/dist/components/rainbow-button/RainbowButton.js +13 -5
  64. package/dist/components/ripple-button/RippleButton.d.ts +20 -1
  65. package/dist/components/ripple-button/RippleButton.js +44 -21
  66. package/dist/components/shimmer-button/ShimmerButton.d.ts +5 -5
  67. package/dist/components/shimmer-button/ShimmerButton.js +17 -19
  68. package/dist/components/smooth-cursor/SmoothCursor.d.ts +13 -9
  69. package/dist/components/smooth-cursor/SmoothCursor.js +114 -163
  70. package/dist/components/smooth-cursor/smooth-cursor-core.d.ts +85 -0
  71. package/dist/components/smooth-cursor/smooth-cursor-core.js +327 -0
  72. package/dist/components/stepper/Step.js +32 -19
  73. package/dist/components/stepper/Stepper.js +18 -2
  74. package/dist/components/stepper/types.d.ts +13 -0
  75. package/dist/components/timeline/Timeline.d.ts +8 -1
  76. package/dist/components/timeline/Timeline.js +165 -76
  77. package/dist/index.d.ts +1 -0
  78. package/dist/index.js +6 -1
  79. package/dist/internals/markdown.d.ts +5 -0
  80. package/dist/internals/motion/haptics.d.ts +0 -4
  81. package/dist/internals/motion/presence-core.d.ts +59 -0
  82. package/dist/internals/motion/presence-core.js +162 -0
  83. package/dist/internals/motion/presence.d.ts +2 -4
  84. package/dist/internals/motion/presence.js +1 -156
  85. package/dist/internals/motion/raf.d.ts +1 -1
  86. package/dist/internals/motion/stagger.d.ts +3 -4
  87. package/dist/internals/motion/tokens.js +4 -0
  88. package/dist/internals/motion/transitions.d.ts +17 -17
  89. package/dist/internals/motion/types.d.ts +6 -7
  90. package/dist/internals/relative-time.d.ts +1 -1
  91. package/dist/styles.css +3545 -204
  92. package/package.json +18 -2
  93. package/dist/components/fireworks-hdr/webgpu-types.d.ts +0 -159
@@ -1,26 +1,158 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { useRef, useState, useEffect } from "react";
3
4
  import { cn } from "../../utils.js";
5
+ import { useReducedMotion } from "../../internals/motion/media-query.js";
4
6
  /* empty css */
7
+ const EASE_TAU = 140;
8
+ const SETTLE_EPSILON = 2e-3;
9
+ const DEFAULT_DURATION_MS = 4e4;
10
+ function parseDuration(raw) {
11
+ const match = /^\s*([\d.]+)\s*(ms|s)\s*$/i.exec(raw);
12
+ if (!match) return DEFAULT_DURATION_MS;
13
+ const value = Number.parseFloat(match[1] ?? "");
14
+ if (!Number.isFinite(value) || value <= 0) return DEFAULT_DURATION_MS;
15
+ return (match[2] ?? "s").toLowerCase() === "ms" ? value : value * 1e3;
16
+ }
17
+ function createConveyor() {
18
+ const c = {
19
+ speed: 1,
20
+ pauseOnHover: false,
21
+ engaged: false,
22
+ factor: 1,
23
+ animations: [],
24
+ rafId: 0,
25
+ lastTs: 0,
26
+ resumeAt: null,
27
+ applyRate() {
28
+ const rate = c.speed * c.factor;
29
+ for (const animation of c.animations) animation.playbackRate = rate;
30
+ },
31
+ wake() {
32
+ if (!c.animations.length || c.rafId) return;
33
+ c.lastTs = 0;
34
+ c.rafId = requestAnimationFrame(tick);
35
+ },
36
+ setEngaged(next) {
37
+ c.engaged = next && c.pauseOnHover;
38
+ c.wake();
39
+ }
40
+ };
41
+ function tick(ts) {
42
+ const dt = c.lastTs ? Math.min(ts - c.lastTs, 64) : 16;
43
+ c.lastTs = ts;
44
+ const target = c.engaged ? 0 : 1;
45
+ c.factor += (target - c.factor) * (1 - Math.exp(-dt / EASE_TAU));
46
+ if (Math.abs(target - c.factor) < SETTLE_EPSILON) c.factor = target;
47
+ c.applyRate();
48
+ if (c.factor === target) {
49
+ c.rafId = 0;
50
+ c.lastTs = 0;
51
+ return;
52
+ }
53
+ c.rafId = requestAnimationFrame(tick);
54
+ }
55
+ return c;
56
+ }
5
57
  function Marquee({
6
58
  className,
7
59
  reverse = false,
8
60
  pauseOnHover = false,
9
61
  vertical = false,
10
62
  repeat = 4,
63
+ fade = true,
64
+ speed = 1,
11
65
  children
12
66
  }) {
67
+ const reduced = useReducedMotion();
68
+ const rootRef = useRef(null);
69
+ const [upgraded, setUpgraded] = useState(false);
70
+ const safeSpeed = Number.isFinite(speed) && speed > 0 ? speed : 1;
71
+ const conveyorRef = useRef(null);
72
+ if (!conveyorRef.current) conveyorRef.current = createConveyor();
73
+ const conveyor = conveyorRef.current;
74
+ useEffect(() => {
75
+ conveyor.speed = safeSpeed;
76
+ conveyor.applyRate();
77
+ }, [conveyor, safeSpeed]);
78
+ useEffect(() => {
79
+ conveyor.pauseOnHover = pauseOnHover;
80
+ if (!pauseOnHover && conveyor.engaged) conveyor.setEngaged(false);
81
+ }, [conveyor, pauseOnHover]);
82
+ useEffect(() => {
83
+ const el = rootRef.current;
84
+ if (!el || reduced) return;
85
+ if (typeof Element === "undefined" || typeof Element.prototype.animate !== "function") return;
86
+ const styles = getComputedStyle(el);
87
+ const duration = parseDuration(styles.getPropertyValue("--duration"));
88
+ const gapPx = Number.parseFloat(vertical ? styles.rowGap : styles.columnGap) || 0;
89
+ const axis = vertical ? "translateY" : "translateX";
90
+ const keyframes = [
91
+ { transform: `${axis}(0)` },
92
+ { transform: `${axis}(calc(-100% - ${gapPx}px))` }
93
+ ];
94
+ const tracks = Array.from(el.querySelectorAll(":scope > [data-marquee-track]"));
95
+ let startTime = 0;
96
+ if (conveyor.resumeAt !== null) {
97
+ startTime = (reverse ? 1 - conveyor.resumeAt : conveyor.resumeAt) * duration;
98
+ } else {
99
+ const running = tracks[0]?.getAnimations?.()[0];
100
+ if (running && typeof running.currentTime === "number") {
101
+ startTime = running.currentTime * conveyor.speed;
102
+ }
103
+ }
104
+ conveyor.animations = tracks.map(
105
+ (track) => track.animate(keyframes, {
106
+ duration,
107
+ iterations: Infinity,
108
+ easing: "linear",
109
+ direction: reverse ? "reverse" : "normal"
110
+ })
111
+ ).filter(Boolean);
112
+ for (const animation of conveyor.animations) animation.currentTime = startTime % duration;
113
+ conveyor.applyRate();
114
+ setUpgraded(true);
115
+ if (conveyor.factor !== (conveyor.engaged ? 0 : 1)) conveyor.wake();
116
+ return () => {
117
+ if (conveyor.rafId) cancelAnimationFrame(conveyor.rafId);
118
+ conveyor.rafId = 0;
119
+ const t = conveyor.animations[0]?.currentTime;
120
+ if (typeof t === "number") {
121
+ const progress = t % duration / duration;
122
+ conveyor.resumeAt = reverse ? 1 - progress : progress;
123
+ } else {
124
+ conveyor.resumeAt = null;
125
+ }
126
+ for (const animation of conveyor.animations) animation.cancel?.();
127
+ conveyor.animations = [];
128
+ setUpgraded(false);
129
+ };
130
+ }, [conveyor, vertical, reverse, repeat, className, reduced]);
131
+ function onFocusOut(event) {
132
+ if (!rootRef.current?.contains(event.relatedTarget)) conveyor.setEngaged(false);
133
+ }
13
134
  return /* @__PURE__ */ jsx(
14
135
  "div",
15
136
  {
137
+ ref: rootRef,
16
138
  className: cn(
17
- "fancy-marquee group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]",
18
- vertical ? "flex-col" : "flex-row",
139
+ "marquee group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]",
140
+ vertical ? "marquee-vertical flex-col" : "flex-row",
141
+ fade && "marquee-fade",
142
+ upgraded && "marquee-upgraded",
19
143
  className
20
144
  ),
145
+ style: { "--marquee-speed": safeSpeed },
146
+ "data-paused-on-hover": pauseOnHover ? "" : void 0,
147
+ onPointerEnter: () => conveyor.setEngaged(true),
148
+ onPointerLeave: () => conveyor.setEngaged(false),
149
+ onFocus: () => conveyor.setEngaged(true),
150
+ onBlur: onFocusOut,
21
151
  children: Array.from({ length: repeat }, (_, index) => /* @__PURE__ */ jsx(
22
152
  "div",
23
153
  {
154
+ "data-marquee-track": "",
155
+ "aria-hidden": index > 0 ? "true" : void 0,
24
156
  className: cn(
25
157
  "flex shrink-0 justify-around [gap:var(--gap)]",
26
158
  vertical ? "animate-marquee-vertical flex-col" : "animate-marquee flex-row",
@@ -4,5 +4,6 @@ export interface ReviewCardProps {
4
4
  username: string;
5
5
  body: string;
6
6
  }
7
- /** A ready-made card for testimonial-style marquees. */
7
+ /** A ready-made card for testimonial-style marquees: a nested double frame,
8
+ * a hairline shell holding a hairline inner panel. */
8
9
  export declare function ReviewCard({ img, name, username, body }: ReviewCardProps): import("react").JSX.Element;
@@ -1,16 +1,27 @@
1
1
  "use client";
2
- import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ /* empty css */
3
4
  function ReviewCard({ img, name, username, body }) {
4
- return /* @__PURE__ */ jsxs("figure", { className: "relative w-64 cursor-pointer overflow-hidden rounded-xl border border-gray-950/[.1] bg-gray-950/[.01] p-4 hover:bg-gray-950/[.05] dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]", children: [
5
- /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center gap-2", children: [
6
- /* @__PURE__ */ jsx("img", { src: img, className: "rounded-full", width: "32", height: "32", alt: "" }),
7
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
8
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium dark:text-white", children: name }),
9
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium dark:text-white/40", children: username })
5
+ return /* @__PURE__ */ jsx("figure", { className: "review-card relative flex w-72 shrink-0 flex-col rounded-2xl border p-1", children: /* @__PURE__ */ jsxs("div", { className: "review-card-inner relative flex-1 overflow-hidden rounded-[12px] border px-4 pt-3.5 pb-4", children: [
6
+ /* @__PURE__ */ jsx("span", { className: "review-card-sheen", "aria-hidden": "true" }),
7
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-3", children: [
8
+ /* @__PURE__ */ jsx(
9
+ "img",
10
+ {
11
+ src: img,
12
+ className: "review-card-avatar size-8 rounded-full",
13
+ width: "32",
14
+ height: "32",
15
+ alt: ""
16
+ }
17
+ ),
18
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col leading-tight", children: [
19
+ /* @__PURE__ */ jsx("span", { className: "review-card-name truncate text-[13px] font-medium tracking-[-0.01em]", children: name }),
20
+ /* @__PURE__ */ jsx("span", { className: "review-card-handle truncate text-[12px]", children: username })
10
21
  ] })
11
22
  ] }),
12
- /* @__PURE__ */ jsx("blockquote", { className: "mt-2 text-sm", children: body })
13
- ] });
23
+ /* @__PURE__ */ jsx("blockquote", { className: "review-card-body relative mt-3 text-[13px] leading-relaxed", children: body })
24
+ ] }) });
14
25
  }
15
26
  export {
16
27
  ReviewCard
@@ -1,21 +1,43 @@
1
1
  /**
2
2
  * Meteors - Animated meteor shower effect
3
3
  *
4
- * Generates N span elements with randomized positions, delays, and durations
5
- * that animate diagonally across the container.
4
+ * A seeded field of meteors, each at its own depth: near ones are larger,
5
+ * brighter, longer-tailed and faster, far ones faint and slow, so the
6
+ * shower reads in parallax. Each meteor fades in, streaks across on a
7
+ * diagonal and burns out; a few flare before they go.
6
8
  */
7
9
  export interface MeteorsProps {
8
10
  /** Number of meteors to render */
9
11
  count?: number;
10
12
  /** Additional CSS classes applied to each meteor */
11
13
  className?: string;
12
- /**
13
- * Seed for the meteor layout. The same seed always produces the same
14
- * shower, which is what keeps a server render and its hydration
15
- * identical. Change it to give two showers on one page different
16
- * layouts — the default is shared, so two `<Meteors />` with no seed
17
- * fall the same way.
18
- */
14
+ /** Direction of travel, in degrees (215 = down and to the right) */
15
+ angle?: number;
16
+ /** Speed multiplier: 2 is twice as fast */
17
+ speed?: number;
18
+ /** Head and tail colour. Defaults to a pale blue-white on dark pages, slate on light ones */
19
+ color?: string;
20
+ /** Seed for the field — same seed, same shower (and the same markup on server and client) */
19
21
  seed?: number;
20
22
  }
21
- export declare function Meteors({ count, className, seed }: MeteorsProps): import("react").JSX.Element;
23
+ export interface Meteor {
24
+ /** Start position, % of the container */
25
+ left: number;
26
+ top: number;
27
+ /** 0 (far) to 1 (near) */
28
+ depth: number;
29
+ /** Seconds for one pass */
30
+ duration: number;
31
+ /** Negative start offset, so the shower is already under way */
32
+ delay: number;
33
+ /** Whether this one flares before it burns out */
34
+ flare: boolean;
35
+ }
36
+ /**
37
+ * The shower. Starts reach well past the left edge (meteors drift right
38
+ * as they fall, so a start on the left is what fills the left half) and
39
+ * over the upper part of the height; depth skewed toward far, the way a
40
+ * real sky has many faint meteors and few bright ones.
41
+ */
42
+ export declare function meteorField(count: number, seed?: number): Meteor[];
43
+ export declare function Meteors({ count, className, angle, speed, color, seed, }: MeteorsProps): import("react").JSX.Element;
@@ -4,42 +4,75 @@ import { useMemo } from "react";
4
4
  import { cn } from "../../utils.js";
5
5
  /* empty css */
6
6
  function mulberry32(seed) {
7
- let state = seed >>> 0;
7
+ let a = seed >>> 0;
8
8
  return () => {
9
- state = state + 1831565813 >>> 0;
10
- let t = state;
9
+ a = a + 1831565813 >>> 0;
10
+ let t = a;
11
11
  t = Math.imul(t ^ t >>> 15, t | 1);
12
12
  t ^= t + Math.imul(t ^ t >>> 7, t | 61);
13
13
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
14
14
  };
15
15
  }
16
- function createMeteors(count, seed) {
17
- const random = mulberry32(seed);
18
- return Array.from({ length: count }, () => ({
19
- left: `${Math.floor(random() * 800 - 400)}px`,
20
- animationDelay: `${(random() * 0.6 + 0.2).toFixed(2)}s`,
21
- animationDuration: `${Math.floor(random() * 8 + 2)}s`
22
- }));
16
+ function meteorField(count, seed = 1) {
17
+ const n = Math.max(0, Math.floor(Number.isFinite(count) ? count : 0));
18
+ const rand = mulberry32(seed);
19
+ return Array.from({ length: n }, () => {
20
+ const depth = Math.pow(rand(), 1.6);
21
+ const duration = 7 - depth * 4.5 + rand() * 1.5;
22
+ return {
23
+ left: -50 + rand() * 150,
24
+ top: -30 + rand() * 60,
25
+ depth,
26
+ duration,
27
+ delay: -rand() * duration * 1.6,
28
+ flare: rand() < 0.2
29
+ };
30
+ });
23
31
  }
24
- function Meteors({ count = 20, className, seed = 1 }) {
25
- const meteors = useMemo(() => createMeteors(count, seed), [count, seed]);
32
+ function styleFor(m, speed, angle, color) {
33
+ const scale = 0.55 + m.depth * 0.9;
34
+ const tail = Math.round(40 + m.depth * 110);
35
+ const brightness = (0.35 + m.depth * 0.65).toFixed(2);
36
+ const style = {
37
+ left: `${m.left.toFixed(2)}%`,
38
+ top: `${m.top.toFixed(2)}%`,
39
+ animationDelay: `${(m.delay / speed).toFixed(2)}s`,
40
+ animationDuration: `${(m.duration / speed).toFixed(2)}s`,
41
+ "--meteor-angle": `${angle}deg`,
42
+ "--meteor-scale": scale.toFixed(2),
43
+ "--meteor-tail": `${tail}px`,
44
+ "--meteor-brightness": brightness,
45
+ "--meteor-travel": `${Math.round(700 + m.depth * 500)}px`
46
+ };
47
+ if (color) style["--meteor-color"] = color;
48
+ return style;
49
+ }
50
+ function Meteors({
51
+ count = 20,
52
+ className,
53
+ angle = 215,
54
+ speed = 1,
55
+ color,
56
+ seed = 1
57
+ }) {
58
+ const meteors = useMemo(() => meteorField(count, seed), [count, seed]);
59
+ const speedC = Number.isFinite(speed) && speed > 0 ? speed : 1;
60
+ const angleC = Number.isFinite(angle) ? angle : 215;
26
61
  return /* @__PURE__ */ jsx(Fragment, { children: meteors.map((meteor, i) => /* @__PURE__ */ jsx(
27
62
  "span",
28
63
  {
29
64
  className: cn(
30
- "meteor pointer-events-none absolute top-0 h-0.5 w-0.5 rounded-full bg-slate-500 opacity-0 shadow-[0_0_0_1px_#ffffff10]",
31
- "before:absolute before:top-1/2 before:h-px before:w-[50px] before:-translate-y-1/2 before:bg-gradient-to-r before:from-slate-500 before:to-transparent before:content-['']",
65
+ "meteor pointer-events-none absolute top-0 h-0.5 w-0.5 rounded-full opacity-0",
66
+ meteor.flare && "meteor--flare",
32
67
  className
33
68
  ),
34
- style: {
35
- left: meteor.left,
36
- animationDelay: meteor.animationDelay,
37
- animationDuration: meteor.animationDuration
38
- }
69
+ style: styleFor(meteor, speedC, angleC, color),
70
+ "aria-hidden": "true"
39
71
  },
40
72
  i
41
73
  )) });
42
74
  }
43
75
  export {
44
- Meteors
76
+ Meteors,
77
+ meteorField
45
78
  };
@@ -1,16 +1,17 @@
1
1
  /**
2
- * MosaicGlow core — pure math, no DOM.
2
+ * MosaicGlow core — the framework-free half of the component.
3
3
  *
4
- * Everything the canvas loop needs that can be unit-tested without a browser:
5
- * a seeded PRNG, per-tile grid state, the halo falloff curve, the fps-independent
6
- * heat step, ambient flicker, the colour LUT and the idle drift path.
4
+ * First the pure math, unit-testable without a browser: a seeded PRNG, per-tile
5
+ * grid state, the halo falloff curve, the fps-independent heat step, ambient
6
+ * flicker, the colour LUT and the idle drift path. Then `createMosaicGlow`, the
7
+ * engine that owns the canvas, the rAF loop and the pointer listeners. No
8
+ * framework imports, and nothing touches `window` or `document` at module scope,
9
+ * so the file is safe to import on the server.
7
10
  *
8
- * Ported unchanged from the Svelte package's `mosaic-glow-core.ts`. The only
9
- * edits are the `!` assertions on typed-array, string and regex-group reads:
10
- * this package compiles with `noUncheckedIndexedAccess`, which widens every
11
- * indexed read to `| undefined`. Each one is in bounds by construction (the
12
- * loops are driven by the array's own length, the regex groups are guaranteed
13
- * by a successful match), so the assertions change no behaviour.
11
+ * Compiles under `noUncheckedIndexedAccess`, which widens every indexed read
12
+ * to `| undefined`. The `!` assertions below are all in bounds by construction
13
+ * (the loops are driven by the array's own length, the regex groups are
14
+ * guaranteed by a successful match), so they change no behaviour.
14
15
  */
15
16
  export type Rgb = [number, number, number];
16
17
  /** Small, fast, seedable PRNG (32-bit state). Same seed → same sequence. */
@@ -92,3 +93,90 @@ export declare function releaseTau(trail: number): number;
92
93
  /** `smoothing` 0..1 → pointer lag time constant in seconds (0 = instant). */
93
94
  export declare function smoothingTau(smoothing: number): number;
94
95
  export declare function clamp01(v: number): number;
96
+ /** What the halo does with no pointer: wander on its own or switch off. */
97
+ export type MosaicGlowIdle = "drift" | "none";
98
+ /** Wait after the pointer leaves before the idle drift takes over (ms). */
99
+ export declare const IDLE_DELAY_MS = 1500;
100
+ /** Ambient-only frames are drawn at most this often (ms). */
101
+ export declare const AMBIENT_FRAME_MS = 50;
102
+ /** Chance per second that a tile re-rolls its resting level. */
103
+ export declare const FLICKER_CHANCE = 0.15;
104
+ /** Steps in the tile colour ramp. */
105
+ export declare const LUT_SIZE = 64;
106
+ /** `tileSize` → tile edge in CSS px. */
107
+ export declare function normalizeTileSize(v: number): number;
108
+ /** `gap` → gap between tiles in CSS px. */
109
+ export declare function normalizeGap(v: number): number;
110
+ /** `radius` → halo radius in CSS px. */
111
+ export declare function normalizeRadius(v: number): number;
112
+ export interface MosaicGlowElements {
113
+ /** Positioned host: measured for the backing store, and the pointer surface. */
114
+ host: HTMLElement;
115
+ /** Canvas the mosaic is painted on. The engine owns its width/height. */
116
+ canvas: HTMLCanvasElement;
117
+ }
118
+ /**
119
+ * Mount-only options. MosaicGlow has none — every prop the wrapper passes is
120
+ * reacted to after mount — so the split is recorded here as "nothing is frozen".
121
+ */
122
+ export type MosaicGlowInitOptions = Record<never, never>;
123
+ /**
124
+ * Options the engine reacts to after mount. `setOptions` acts on the keys that
125
+ * are present (an absent or `undefined` key is left alone), grouped by concern
126
+ * exactly as the wrapper's effects are:
127
+ * structural (`tileSize`, `gap`, `seed`, `noise`, `ambient`) rebuilds the grid,
128
+ * colour (`color`, `background`) drops the ramp cache, visual (`idle`,
129
+ * `flicker`, `radius`, `intensity`) repaints, `reducedMotion` swaps the loop for
130
+ * a single static frame, `interactive` attaches or detaches the pointer
131
+ * listeners, `visible` pauses and resumes the loop. `trail` and `smoothing` are
132
+ * read by the next frame and schedule nothing.
133
+ */
134
+ export interface MosaicGlowLiveOptions {
135
+ /** Tile edge in CSS px. */
136
+ tileSize?: number;
137
+ /** Gap between tiles in CSS px. */
138
+ gap?: number;
139
+ /** Halo / tile colour — hex or rgb(). */
140
+ color?: string;
141
+ /** Surface colour behind the tiles — hex or rgb(). */
142
+ background?: string;
143
+ /** Halo radius in CSS px. */
144
+ radius?: number;
145
+ /** Overall brightness of lit tiles, 0–1. */
146
+ intensity?: number;
147
+ /** How long lit tiles linger after the halo moves on, 0–1. */
148
+ trail?: number;
149
+ /** Pointer lag, 0 (instant) to 1 (very laggy). */
150
+ smoothing?: number;
151
+ /** Spread of per-tile random brightness inside the halo, 0–1. */
152
+ noise?: number;
153
+ /** Visibility of the random faint tiles outside the halo, 0–1. */
154
+ ambient?: number;
155
+ /** Slowly re-roll the faint tiles over time. */
156
+ flicker?: boolean;
157
+ /** What the halo does with no pointer. */
158
+ idle?: MosaicGlowIdle;
159
+ /** Follow the pointer. Off leaves only the idle behaviour. */
160
+ interactive?: boolean;
161
+ /** Seed for the per-tile randomness — same seed, same mosaic. */
162
+ seed?: number;
163
+ /** The wrapper's `prefers-reduced-motion` gate: no loop, one settled frame. */
164
+ reducedMotion?: boolean;
165
+ /** The wrapper's visibility gate: false parks the loop. */
166
+ visible?: boolean;
167
+ }
168
+ export interface MosaicGlowEngine {
169
+ setOptions(next: Partial<MosaicGlowLiveOptions>): void;
170
+ /** Re-measure the host; rebuilds only when the box or the pixel ratio moved. */
171
+ resize(): void;
172
+ destroy(): void;
173
+ }
174
+ /**
175
+ * Drive a MosaicGlow canvas. Returns null when no 2D context is available
176
+ * (server, jsdom without a stub, a browser that refuses the context) — the
177
+ * caller then simply renders an inert canvas.
178
+ *
179
+ * `random` feeds the ambient flicker only; the per-tile field is seeded through
180
+ * the `seed` option and stays deterministic.
181
+ */
182
+ export declare function createMosaicGlow(el: MosaicGlowElements, options?: MosaicGlowInitOptions & MosaicGlowLiveOptions, random?: () => number): MosaicGlowEngine | null;
@@ -1,22 +1,41 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
2
  /**
3
- * NeonBorder - Dual-color neon glow border effect
3
+ * NeonBorder - a two-colour neon tube around its content
4
4
  *
5
- * Two gradient layers with blur and drop-shadow create a bicolor neon effect.
6
- * Optional rotation animation with half or full coverage.
5
+ * A faint tube runs all the way round; two beams, one per colour, chase
6
+ * each other along it from opposite sides. Each beam has a white-hot core
7
+ * and a coloured glow that spills both inside and outside the edge. The
8
+ * glow hums (a slow breathing), and the tube flickers once as it ignites.
7
9
  */
8
10
  export interface NeonBorderProps extends Omit<HTMLAttributes<HTMLDivElement>, "className" | "style"> {
9
11
  /** First neon color */
10
12
  color1?: string;
11
13
  /** Second neon color */
12
14
  color2?: string;
13
- /** Animation type: none (static), half (50% coverage), full (100% coverage) */
15
+ /**
16
+ * How much of the tube is lit: none (two short beams, static), half
17
+ * (two short beams chasing), full (two long beams that nearly close the ring)
18
+ */
14
19
  animationType?: "none" | "half" | "full";
15
- /** Animation duration in seconds */
20
+ /** Time for the beams to travel once around, in seconds */
16
21
  duration?: number;
17
22
  /** Additional CSS classes */
18
23
  className?: string;
19
24
  /** Content */
20
25
  children?: ReactNode;
21
26
  }
27
+ export type NeonAnimationType = NonNullable<NeonBorderProps["animationType"]>;
28
+ /** Share of the turn each beam covers, in %. */
29
+ export declare function beamArc(type: NeonAnimationType): number;
30
+ /**
31
+ * The static tube (`animationType="none"`): lit at two opposite corners,
32
+ * the first colour top-left and the second bottom-right.
33
+ */
34
+ export declare function neonCorners(color1: string, color2: string, core?: boolean): string;
35
+ /**
36
+ * The two beams as one conic gradient: each fades in from its tail and
37
+ * cuts off sharply at its head, the second half a turn behind the first.
38
+ * `core` whitens each colour for the hot centre of the tube.
39
+ */
40
+ export declare function neonBeams(color1: string, color2: string, arc: number, core?: boolean): string;
22
41
  export declare function NeonBorder({ color1, color2, animationType, duration, className, children, ...rest }: NeonBorderProps): import("react").JSX.Element;
@@ -2,6 +2,26 @@
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import { cn } from "../../utils.js";
4
4
  /* empty css */
5
+ function beamArc(type) {
6
+ return type === "full" ? 46 : 22;
7
+ }
8
+ function neonCorners(color1, color2, core = false) {
9
+ const tint = (c) => core ? `color-mix(in srgb, ${c} 45%, #fff)` : c;
10
+ return `linear-gradient(135deg, ${tint(color1)} 0%, transparent 32%, transparent 68%, ${tint(color2)} 100%)`;
11
+ }
12
+ function neonBeams(color1, color2, arc, core = false) {
13
+ const tint = (c) => core ? `color-mix(in srgb, ${c} 45%, #fff)` : c;
14
+ const beam = (c, from) => {
15
+ const head = from + arc;
16
+ return [
17
+ `transparent ${from}%`,
18
+ `color-mix(in srgb, ${tint(c)} 55%, transparent) ${(from + arc * 0.45).toFixed(1)}%`,
19
+ `${tint(c)} ${(head - 0.6).toFixed(1)}%`,
20
+ `transparent ${head.toFixed(1)}%`
21
+ ].join(", ");
22
+ };
23
+ return `conic-gradient(from var(--neon-angle), ${beam(color1, 0)}, ${beam(color2, 50)}, transparent 100%)`;
24
+ }
5
25
  function getWidth(type) {
6
26
  switch (type) {
7
27
  case "none":
@@ -21,30 +41,40 @@ function NeonBorder({
21
41
  children,
22
42
  ...rest
23
43
  }) {
44
+ const arc = beamArc(animationType);
45
+ const animated = animationType !== "none";
24
46
  const styleVars = {
25
47
  "--neon-duration": `${duration}s`,
26
48
  "--neon-color1": color1,
27
49
  "--neon-color2": color2,
28
- "--neon-width": `${getWidth(animationType)}%`
50
+ "--neon-width": `${getWidth(animationType)}%`,
51
+ "--neon-beams": animated ? neonBeams(color1, color2, arc) : neonCorners(color1, color2),
52
+ "--neon-core": animated ? neonBeams(color1, color2, arc, true) : neonCorners(color1, color2, true)
29
53
  };
30
- const animated = animationType !== "none";
31
54
  return /* @__PURE__ */ jsxs(
32
55
  "div",
33
56
  {
34
57
  className: cn(
35
- "neon-border-container relative z-10 inline-block h-10 w-full max-w-sm overflow-hidden rounded-lg p-px",
58
+ "neon-border-container relative z-10 inline-block h-10 w-full max-w-sm rounded-lg p-px",
59
+ animated && "neon-animated",
36
60
  className
37
61
  ),
38
62
  style: styleVars,
39
63
  ...rest,
40
64
  children: [
41
- /* @__PURE__ */ jsx("div", { className: cn("neon-layer-one rounded-lg", animated && "neon-animated") }),
42
- /* @__PURE__ */ jsx("div", { className: cn("neon-layer-two rounded-lg", animated && "neon-animated") }),
43
- children
65
+ children,
66
+ /* @__PURE__ */ jsx("span", { className: "neon-tube", "aria-hidden": "true" }),
67
+ /* @__PURE__ */ jsxs("span", { className: "neon-light", "aria-hidden": "true", children: [
68
+ /* @__PURE__ */ jsx("span", { className: "neon-glow", children: /* @__PURE__ */ jsx("span", {}) }),
69
+ /* @__PURE__ */ jsx("span", { className: "neon-core" })
70
+ ] })
44
71
  ]
45
72
  }
46
73
  );
47
74
  }
48
75
  export {
49
- NeonBorder
76
+ NeonBorder,
77
+ beamArc,
78
+ neonBeams,
79
+ neonCorners
50
80
  };