apex-ui-kit 2.6.0 → 2.8.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 (33) hide show
  1. package/ReadMe.md +32 -10
  2. package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +340 -161
  3. package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
  4. package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +356 -0
  5. package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +22 -2
  6. package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +381 -0
  7. package/dist/ApexUI-Kit/templates/CommandPalette/CommandPalette.jsx +250 -0
  8. package/dist/ApexUI-Kit/templates/CrossedMarquee/CrossedMarquee.jsx +158 -0
  9. package/dist/ApexUI-Kit/templates/DitherWaves/DitherWaves.jsx +207 -0
  10. package/dist/ApexUI-Kit/templates/EdgeBlur/EdgeBlur.jsx +92 -0
  11. package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +5 -1
  12. package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +274 -0
  13. package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +2 -2
  14. package/dist/ApexUI-Kit/templates/HorizonGlow/HorizonGlow.jsx +259 -0
  15. package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +116 -0
  16. package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +230 -0
  17. package/dist/ApexUI-Kit/templates/MagneticButton/MagneticButton.jsx +69 -0
  18. package/dist/ApexUI-Kit/templates/Marquee3D/Marquee3D.jsx +68 -0
  19. package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +123 -1
  20. package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
  21. package/dist/ApexUI-Kit/templates/ParticleText/ParticleText.jsx +328 -0
  22. package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +119 -0
  23. package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +88 -0
  24. package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +22 -1
  25. package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +290 -0
  26. package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +18 -3
  27. package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +215 -0
  28. package/dist/ApexUI-Kit/templates/ThemeSwitcher/themes.js +38 -0
  29. package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +21 -13
  30. package/dist/ApexUI-Kit/templates/WireframeKnot3D/WireframeKnot3D.jsx +112 -0
  31. package/dist/cli/index.js +20 -0
  32. package/package.json +2 -2
  33. package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.css +0 -1
@@ -0,0 +1,274 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBColorSpace, TextureLoader, Vector2, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Glass Lens: a drop of liquid glass that glides over an image, drawn in
6
+ * WebGL. It magnifies the middle, bends the picture hard at its rim, splits
7
+ * the colors a little at the edge, catches a specular highlight and casts a
8
+ * soft shadow. It follows the pointer on a spring and stretches along the
9
+ * way it moves, then rounds out again when it stops. When the pointer
10
+ * leaves it drifts on its own. Drag it on touch screens.
11
+ */
12
+
13
+ const DEFAULT_IMAGE = "https://ik.imagekit.io/ny6yinyut/apexui/assets/componentsimage/galaxy-3d.png?tr=f-auto,q-85,w-1600";
14
+
15
+ const VERT = `void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }`;
16
+
17
+ const FRAG = `
18
+ precision highp float;
19
+ uniform sampler2D uTex; uniform vec2 uRes; uniform vec2 uCover; uniform float uReady;
20
+ uniform vec2 uLens; uniform float uRadius; uniform vec2 uStretch; uniform float uAngle;
21
+ uniform float uRefract; uniform float uDisperse; uniform float uFrost; uniform vec3 uTint; uniform float uTintAmt;
22
+
23
+ vec3 img(vec2 uv){ return texture2D(uTex, (uv - 0.5) * uCover + 0.5).rgb; }
24
+
25
+ void main(){
26
+ vec2 px = gl_FragCoord.xy;
27
+ vec2 uv = px / uRes;
28
+ vec3 base = img(uv);
29
+
30
+ // Lens space: rotate into the direction of travel and squash by the stretch.
31
+ vec2 rel = px - uLens;
32
+ float c = cos(uAngle); float s = sin(uAngle);
33
+ vec2 q = vec2(c * rel.x + s * rel.y, -s * rel.x + c * rel.y) / (uRadius * uStretch);
34
+ float d = length(q);
35
+
36
+ // Soft drop shadow just outside the rim.
37
+ float shadow = smoothstep(1.45, 0.95, d) * (1.0 - smoothstep(0.98, 1.0, d));
38
+ vec3 col = base * (1.0 - 0.28 * shadow);
39
+
40
+ if (d < 1.0) {
41
+ float h = sqrt(1.0 - d * d); // dome height
42
+ vec2 dir = rel / max(length(rel), 1e-3);
43
+ // Like a convex lens: the middle is magnified, the rim squeezes in the
44
+ // picture from further out.
45
+ float rimK = (1.0 - h) * (1.0 - h);
46
+ float scaleK = mix(1.0, 0.7, uRefract) + rimK * 1.15 * uRefract;
47
+ vec2 baseUv = (uLens + rel * scaleK) / uRes;
48
+ float disp = uDisperse * rimK * 0.006;
49
+ vec3 g;
50
+ if (uFrost > 0.0) {
51
+ vec3 acc = vec3(0.0);
52
+ float k = uFrost * 0.012;
53
+ for (int i = 0; i < 8; i++) {
54
+ float a = float(i) * 0.785398;
55
+ vec2 o = vec2(cos(a), sin(a)) * k * (0.5 + 0.5 * fract(float(i) * 0.618));
56
+ acc += vec3(img(baseUv + o + dir * disp).r, img(baseUv + o).g, img(baseUv + o - dir * disp).b);
57
+ }
58
+ g = acc / 8.0;
59
+ } else {
60
+ g = vec3(img(baseUv + dir * disp).r, img(baseUv).g, img(baseUv - dir * disp).b);
61
+ }
62
+ g = mix(g, g * uTint + uTint * 0.08, uTintAmt);
63
+ // Light from the top left: a tight highlight and a broad sheen.
64
+ vec3 n = normalize(vec3(q, h * 1.4));
65
+ vec3 l = normalize(vec3(-0.45, 0.6, 0.65));
66
+ float spec = pow(max(dot(reflect(-l, n), vec3(0.0, 0.0, 1.0)), 0.0), 48.0);
67
+ float sheen = pow(max(dot(n, l), 0.0), 3.0) * 0.12;
68
+ float rim = smoothstep(0.86, 0.995, d);
69
+ float inner = smoothstep(0.995, 0.9, d) * smoothstep(0.75, 0.97, d);
70
+ g += vec3(1.0) * (spec * 0.9 + sheen) + vec3(1.0) * rim * 0.22 * (0.6 + 0.4 * q.y) - inner * 0.06;
71
+ float edge = smoothstep(1.0, 0.985, d);
72
+ col = mix(col, g, edge);
73
+ }
74
+ col = mix(vec3(0.07), col, uReady);
75
+ gl_FragColor = vec4(col, 1.0);
76
+ }`;
77
+
78
+ const GlassLens = ({ image = DEFAULT_IMAGE, size = 9, refraction = 0.7, dispersion = 1, frost = 0, tint = "#ffffff", tintAmount = 0, stretch = 1, idle = true, height = "28rem", className = "", children }) => {
79
+ const wrap = useRef(null);
80
+ const live = useRef({});
81
+ live.current = { size, refraction, dispersion, frost, tint, tintAmount, stretch, idle };
82
+
83
+ useEffect(() => {
84
+ const el = wrap.current;
85
+ const renderer = new WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" });
86
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
87
+ renderer.setPixelRatio(dpr);
88
+ renderer.outputColorSpace = SRGBColorSpace;
89
+ Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block", touchAction: "none" });
90
+ el.prepend(renderer.domElement);
91
+ const scene = new Scene();
92
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
93
+ const u = {
94
+ uTex: { value: null },
95
+ uRes: { value: new Vector2(1, 1) },
96
+ uCover: { value: new Vector2(1, 1) },
97
+ uReady: { value: 0 },
98
+ uLens: { value: new Vector2() },
99
+ uRadius: { value: 100 },
100
+ uStretch: { value: new Vector2(1, 1) },
101
+ uAngle: { value: 0 },
102
+ uRefract: { value: 0.7 },
103
+ uDisperse: { value: 1 },
104
+ uFrost: { value: 0 },
105
+ uTint: { value: new Color(1, 1, 1) },
106
+ uTintAmt: { value: 0 },
107
+ };
108
+ const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
109
+ const geometry = new PlaneGeometry(2, 2);
110
+ scene.add(new Mesh(geometry, material));
111
+
112
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
113
+ const view = { w: 1, h: 1, aspect: 1, imgAspect: 1 };
114
+ // Lens state in CSS pixels, y measured from the bottom like gl_FragCoord.
115
+ const lens = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0, sx: 1, sy: 1, angle: 0, placed: false };
116
+ const pointer = { inside: false, dragging: false, lastMove: 0 };
117
+ let raf = 0;
118
+ let last = 0;
119
+ let time = 0;
120
+ let visible = true;
121
+ let readyAt = 0;
122
+
123
+ const cover = () => {
124
+ const a = view.aspect;
125
+ const ia = view.imgAspect;
126
+ u.uCover.value.set(ia > a ? a / ia : 1, ia > a ? 1 : ia / a);
127
+ };
128
+ new TextureLoader().setCrossOrigin("anonymous").load(image, (t) => {
129
+ t.colorSpace = SRGBColorSpace;
130
+ u.uTex.value = t;
131
+ view.imgAspect = t.image.width / t.image.height;
132
+ cover();
133
+ readyAt = performance.now();
134
+ start();
135
+ });
136
+
137
+ const fit = () => {
138
+ const { width, height: hh } = el.getBoundingClientRect();
139
+ if (!width || !hh) return;
140
+ view.w = width;
141
+ view.h = hh;
142
+ view.aspect = width / hh;
143
+ renderer.setSize(width, hh, false);
144
+ u.uRes.value.set(width * dpr, hh * dpr);
145
+ cover();
146
+ if (!lens.placed) {
147
+ lens.x = lens.tx = width * 0.5;
148
+ lens.y = lens.ty = hh * 0.5;
149
+ lens.placed = true;
150
+ }
151
+ start();
152
+ };
153
+
154
+ const frame = (now) => {
155
+ raf = 0;
156
+ const o = live.current;
157
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
158
+ last = now;
159
+ time += dt;
160
+ // Idle drift when nobody is steering it.
161
+ if (o.idle && !pointer.inside && !pointer.dragging && !reduced && now - pointer.lastMove > 900) {
162
+ lens.tx = view.w * (0.5 + 0.28 * Math.sin(time * 0.45));
163
+ lens.ty = view.h * (0.5 + 0.22 * Math.sin(time * 0.7 + 1.2));
164
+ }
165
+ // Spring toward the target (slightly underdamped for a liquid feel).
166
+ const k = reduced ? 400 : 140;
167
+ const c = 2 * Math.sqrt(k) * 0.82;
168
+ lens.vx += (k * (lens.tx - lens.x) - c * lens.vx) * dt;
169
+ lens.vy += (k * (lens.ty - lens.y) - c * lens.vy) * dt;
170
+ lens.x += lens.vx * dt;
171
+ lens.y += lens.vy * dt;
172
+ const speed = Math.hypot(lens.vx, lens.vy);
173
+ const goal = 1 + Math.min(0.32, speed / 2600) * o.stretch;
174
+ lens.sx += (goal - lens.sx) * (1 - Math.exp(-dt / 0.08));
175
+ lens.sy = 1 / Math.sqrt(lens.sx);
176
+ if (speed > 30) {
177
+ const want = Math.atan2(lens.vy, lens.vx);
178
+ let delta = want - lens.angle;
179
+ delta = Math.atan2(Math.sin(delta), Math.cos(delta));
180
+ lens.angle += delta * (1 - Math.exp(-dt / 0.06));
181
+ }
182
+
183
+ // size is in rem; never more than ~40% of the short side.
184
+ const r = Math.min(Math.min(view.w, view.h) * 0.4, o.size * 16);
185
+ u.uRadius.value = r * dpr;
186
+ u.uLens.value.set(lens.x * dpr, (view.h - lens.y) * dpr);
187
+ u.uStretch.value.set(lens.sx, lens.sy);
188
+ u.uAngle.value = -lens.angle;
189
+ u.uRefract.value = o.refraction;
190
+ u.uDisperse.value = o.dispersion;
191
+ u.uFrost.value = o.frost;
192
+ u.uTint.value.set(o.tint);
193
+ u.uTintAmt.value = o.tintAmount;
194
+ u.uReady.value = readyAt ? Math.min(1, (now - readyAt) / 500) : 0;
195
+ renderer.render(scene, camera);
196
+
197
+ const moving = speed > 0.5 || Math.abs(lens.tx - lens.x) > 0.3 || Math.abs(lens.ty - lens.y) > 0.3 || Math.abs(lens.sx - 1) > 0.002;
198
+ const idling = o.idle && !pointer.inside && !reduced;
199
+ if (visible && (moving || idling || u.uReady.value < 1)) raf = requestAnimationFrame(frame);
200
+ else last = 0;
201
+ };
202
+ function start() {
203
+ if (!raf && visible) raf = requestAnimationFrame(frame);
204
+ }
205
+
206
+ const local = (e) => {
207
+ const r = el.getBoundingClientRect();
208
+ return [e.clientX - r.left, e.clientY - r.top];
209
+ };
210
+ const onMove = (e) => {
211
+ if (e.pointerType === "touch" && !pointer.dragging) return;
212
+ [lens.tx, lens.ty] = local(e);
213
+ pointer.inside = true;
214
+ pointer.lastMove = performance.now();
215
+ start();
216
+ };
217
+ const onDown = (e) => {
218
+ if (e.pointerType !== "touch") return;
219
+ e.preventDefault();
220
+ pointer.dragging = true;
221
+ el.setPointerCapture(e.pointerId);
222
+ [lens.tx, lens.ty] = local(e);
223
+ start();
224
+ };
225
+ const onUp = () => {
226
+ pointer.dragging = false;
227
+ pointer.lastMove = performance.now();
228
+ };
229
+ const onLeave = (e) => {
230
+ if (e.pointerType === "touch") return;
231
+ pointer.inside = false;
232
+ pointer.lastMove = performance.now();
233
+ start();
234
+ };
235
+ el.addEventListener("pointermove", onMove);
236
+ el.addEventListener("pointerdown", onDown);
237
+ el.addEventListener("pointerup", onUp);
238
+ el.addEventListener("pointercancel", onUp);
239
+ el.addEventListener("pointerleave", onLeave);
240
+ const ro = new ResizeObserver(fit);
241
+ ro.observe(el);
242
+ const io = new IntersectionObserver(([entry]) => {
243
+ visible = entry.isIntersecting;
244
+ if (visible) start();
245
+ });
246
+ io.observe(el);
247
+ fit();
248
+
249
+ return () => {
250
+ cancelAnimationFrame(raf);
251
+ ro.disconnect();
252
+ io.disconnect();
253
+ el.removeEventListener("pointermove", onMove);
254
+ el.removeEventListener("pointerdown", onDown);
255
+ el.removeEventListener("pointerup", onUp);
256
+ el.removeEventListener("pointercancel", onUp);
257
+ el.removeEventListener("pointerleave", onLeave);
258
+ u.uTex.value?.dispose();
259
+ geometry.dispose();
260
+ material.dispose();
261
+ renderer.dispose();
262
+ renderer.forceContextLoss();
263
+ renderer.domElement.remove();
264
+ };
265
+ }, [image]);
266
+
267
+ return (
268
+ <div ref={wrap} className={`relative w-full cursor-none select-none overflow-hidden bg-[#111] ${className}`} style={{ height }}>
269
+ {children && <div className="pointer-events-none relative z-10 h-full w-full">{children}</div>}
270
+ </div>
271
+ );
272
+ };
273
+
274
+ export default GlassLens;
@@ -289,7 +289,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
289
289
 
290
290
  return (
291
291
  <nav className={`relative inline-flex max-w-full ${className}`} aria-label="Main">
292
- <div ref={bar} className={`relative flex items-center gap-0.5 rounded-full p-1.5 ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white shadow-sm ring-1 ring-black/10"}`}>
292
+ <div ref={bar} className={`relative flex items-center gap-0 rounded-full p-1 sm:gap-0.5 sm:p-1.5 ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white shadow-sm ring-1 ring-black/10"}`}>
293
293
  <canvas ref={canvas} aria-hidden="true" className="pointer-events-none absolute" style={{ left: -PAD, top: -PAD, display: fallback ? "none" : "block" }} />
294
294
  {fallback && <span ref={fallbackPill} aria-hidden="true" className="pointer-events-none absolute rounded-full" style={{ background: accentColor }} />}
295
295
  {items.map((item, i) => (
@@ -299,7 +299,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
299
299
  href={item.href}
300
300
  onClick={(e) => select(i, e)}
301
301
  aria-current={i === active ? "page" : undefined}
302
- className={`relative z-[1] shrink-0 rounded-full px-3.5 py-2.5 text-[0.95rem] font-medium outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-current/30 sm:px-5 ${i === active ? "text-neutral-950" : dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
302
+ className={`relative z-[1] shrink-0 rounded-full px-2.5 py-2 text-[0.82rem] font-medium max-[380px]:px-2 max-[380px]:text-[0.76rem] sm:py-2.5 sm:text-[0.95rem] outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-current/30 sm:px-5 ${i === active ? "text-neutral-950" : dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
303
303
  >
304
304
  {item.label}
305
305
  </a>
@@ -0,0 +1,259 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, Vector2, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Horizon Glow: a soft, wide light rising from below the bottom edge, fading
6
+ * from a pale core through an accent band and a mid tone into a deep sky,
7
+ * drawn in one WebGL shader. A thin luminous arc marks the horizon, faint
8
+ * volumetric rays climb out of it and specks of dust drift up through the
9
+ * light. It rises in on mount, breathes slowly and leans toward the pointer;
10
+ * a filmic tone curve and dithering keep the long gradient silky.
11
+ */
12
+
13
+ const PRESETS = {
14
+ lilac: { sky: "#0d1438", mid: "#6f4c86", accent: "#ff9fd2", core: "#fff1f8" },
15
+ ocean: { sky: "#020f1c", mid: "#195a78", accent: "#58e1ff", core: "#e9fbff" },
16
+ ember: { sky: "#100606", mid: "#7a2f1e", accent: "#ff9b54", core: "#fff0dc" },
17
+ mint: { sky: "#04131a", mid: "#22625a", accent: "#6dffc4", core: "#ecfff6" },
18
+ mono: { sky: "#08080a", mid: "#45454d", accent: "#c9c9d6", core: "#ffffff" },
19
+ };
20
+
21
+ const VERT = `void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }`;
22
+
23
+ const FRAG = `
24
+ precision highp float;
25
+ uniform vec2 uRes; uniform float uTime; uniform float uRise; uniform vec2 uMouse;
26
+ uniform vec3 uSky; uniform vec3 uMid; uniform vec3 uAccent; uniform vec3 uCore;
27
+ uniform float uSpread; uniform float uIntensity; uniform float uShimmer; uniform float uBreath; uniform float uGrain;
28
+ uniform float uArc; uniform float uRays; uniform float uDust;
29
+
30
+ float h2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
31
+ float n2(vec2 p){
32
+ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);
33
+ return mix(mix(h2(i), h2(i + vec2(1, 0)), u.x), mix(h2(i + vec2(0, 1)), h2(i + vec2(1, 1)), u.x), u.y);
34
+ }
35
+ float fbm(vec2 p){ float v = 0.0; float a = 0.5; for (int i = 0; i < 4; i++) { v += a * n2(p); p *= 2.02; a *= 0.5; } return v; }
36
+
37
+ // Drifting dust: one speck per cell, rising slowly and twinkling.
38
+ float dust(vec2 p, float scale, float speed, float seed){
39
+ vec2 q = p * scale + vec2(seed, -uTime * speed);
40
+ vec2 id = floor(q); vec2 f = fract(q) - 0.5;
41
+ vec2 o = vec2(h2(id + seed), h2(id + seed + 3.1)) - 0.5;
42
+ float r = length(f - o * 0.7);
43
+ float tw = 0.5 + 0.5 * sin(uTime * (1.5 + h2(id) * 2.5) + h2(id + 9.0) * 6.28);
44
+ return smoothstep(0.06, 0.0, r) * step(0.55, h2(id + seed + 7.7)) * tw;
45
+ }
46
+
47
+ void main(){
48
+ vec2 uv = gl_FragCoord.xy / uRes;
49
+ float asp = uRes.x / uRes.y;
50
+ // Work in height units; wide screens get a wider glow, phones a taller one.
51
+ vec2 p = vec2((uv.x - 0.5) * asp, uv.y);
52
+ float width = max(asp, 0.8) * 0.82;
53
+
54
+ float rise = uRise;
55
+ float breath = 1.0 + uBreath * 0.035 * sin(uTime * 0.55) + uBreath * 0.015 * sin(uTime * 1.3 + 1.7);
56
+ vec2 c = vec2(uMouse.x * 0.06 * asp, mix(-1.1, -0.06, rise) + uMouse.y * 0.025);
57
+
58
+ // Elliptical distance from the light source below the edge.
59
+ vec2 e = (p - c) / vec2(width, 1.0);
60
+ float d0 = length(e);
61
+ // A slow shimmer that bends the falloff a little.
62
+ float s = fbm(vec2(p.x * 1.4, p.y * 1.1 - uTime * 0.035) + uTime * 0.02) - 0.5;
63
+ float d = d0 + s * 0.12 * uShimmer * smoothstep(1.4, 0.3, d0);
64
+
65
+ float reach = uSpread * breath * mix(0.35, 1.0, rise);
66
+ float t = clamp(d / reach, 0.0, 1.0);
67
+
68
+ // Core -> accent band -> mid tone -> sky.
69
+ vec3 col = mix(uCore, uMid, smoothstep(0.0, 0.62, t));
70
+ col = mix(col, uSky, smoothstep(0.38, 1.0, t));
71
+ float band = exp(-pow((t - 0.3) / 0.13, 2.0));
72
+ col = mix(col, col * 0.55 + uAccent * 0.55, band * 0.45);
73
+ col += uCore * exp(-d * d * 9.0 / (reach * reach)) * 0.25 * uIntensity;
74
+
75
+ // Volumetric rays climbing out of the source.
76
+ float ang = atan(p.x - c.x, (p.y - c.y) * 1.6);
77
+ float beams = n2(vec2(ang * 8.0, uTime * 0.07)) * 0.65 + n2(vec2(ang * 21.0, -uTime * 0.05)) * 0.35;
78
+ col += mix(uAccent, uCore, 0.5) * smoothstep(0.5, 1.0, beams) * exp(-t * 2.4) * smoothstep(0.02, 0.2, t) * 0.28 * uRays;
79
+
80
+ // Horizon arc: a thin bright rim where the light meets the edge of the world.
81
+ float pr = 0.9;
82
+ vec2 pc = vec2(c.x, c.y + 0.24 - pr);
83
+ float rd = length((p - pc) / vec2(max(asp, 0.8) * 1.05, 1.0)) - pr;
84
+ float fade = exp(-abs(p.x - c.x) / (width * 0.9));
85
+ col += mix(uAccent, uCore, 0.7) * (exp(-abs(rd) * 140.0) * 0.9 + exp(-max(rd, 0.0) * 14.0) * 0.18) * fade * uArc * rise;
86
+ col *= mix(1.0, 0.82, smoothstep(0.0, -0.08, rd) * uArc);
87
+
88
+ // Dust lit by the glow.
89
+ float lit = exp(-t * 2.2);
90
+ col += uCore * (dust(p, 9.0, 0.05, 1.0) + dust(p, 16.0, 0.09, 7.0) * 0.7) * lit * 0.55 * uDust * rise;
91
+
92
+ col = mix(uSky, col, clamp(uIntensity * (0.25 + 0.75 * rise), 0.0, 1.4));
93
+ // Darker corners at the top keep text readable.
94
+ col *= 1.0 - 0.12 * smoothstep(0.45, 1.1, uv.y) * smoothstep(0.1, 0.9, abs(uv.x - 0.5) * 2.0);
95
+ // Filmic shoulder so the core rolls off instead of clipping.
96
+ col = col / (1.0 + max(col - 0.85, 0.0));
97
+
98
+ col += (h2(gl_FragCoord.xy + fract(uTime * 7.0) * 91.0) - 0.5) * (1.0 + 7.0 * uGrain) / 255.0;
99
+ gl_FragColor = vec4(col, 1.0);
100
+ }`;
101
+
102
+ const HorizonGlow = ({
103
+ preset = "lilac",
104
+ skyColor,
105
+ midColor,
106
+ accentColor,
107
+ coreColor,
108
+ spread = 1.3,
109
+ intensity = 1,
110
+ duration = 2.4,
111
+ autoRise = true,
112
+ breathe = 1,
113
+ shimmer = 1,
114
+ arc = 0.7,
115
+ rays = 1,
116
+ dust = 1,
117
+ grain = 0.4,
118
+ speed = 1,
119
+ interactive = true,
120
+ replay = 0,
121
+ className = "",
122
+ children,
123
+ }) => {
124
+ const wrap = useRef(null);
125
+ const p = PRESETS[preset] ?? PRESETS.lilac;
126
+ const live = useRef({});
127
+ live.current = { sky: skyColor ?? p.sky, mid: midColor ?? p.mid, accent: accentColor ?? p.accent, core: coreColor ?? p.core, spread, intensity, duration, autoRise, breathe, shimmer, arc, rays, dust, grain, speed, interactive };
128
+ const rise = useRef({ start: 0 });
129
+
130
+ useEffect(() => {
131
+ rise.current.start = performance.now();
132
+ }, [replay, autoRise]);
133
+
134
+ useEffect(() => {
135
+ const el = wrap.current;
136
+ const renderer = new WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" });
137
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
138
+ Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block" });
139
+ el.prepend(renderer.domElement);
140
+ const scene = new Scene();
141
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
142
+ const u = {
143
+ uRes: { value: new Vector2(1, 1) },
144
+ uTime: { value: 0 },
145
+ uRise: { value: 0 },
146
+ uMouse: { value: new Vector2() },
147
+ uSky: { value: new Color() },
148
+ uMid: { value: new Color() },
149
+ uAccent: { value: new Color() },
150
+ uCore: { value: new Color() },
151
+ uSpread: { value: 1 },
152
+ uIntensity: { value: 1 },
153
+ uShimmer: { value: 1 },
154
+ uBreath: { value: 1 },
155
+ uGrain: { value: 0.4 },
156
+ uArc: { value: 0.7 },
157
+ uRays: { value: 1 },
158
+ uDust: { value: 1 },
159
+ };
160
+ const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
161
+ const geometry = new PlaneGeometry(2, 2);
162
+ scene.add(new Mesh(geometry, material));
163
+
164
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
165
+ const mouse = { x: 0, y: 0, tx: 0, ty: 0 };
166
+ let raf = 0;
167
+ let last = 0;
168
+ let time = 0;
169
+ let visible = true;
170
+
171
+ const draw = (now) => {
172
+ const o = live.current;
173
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
174
+ last = now;
175
+ time += dt * o.speed;
176
+ const t = o.autoRise && !reduced ? Math.min(1, (now - rise.current.start) / (Math.max(0.1, o.duration) * 1000)) : 1;
177
+ // Ease out with a touch of overshoot-free settle.
178
+ u.uRise.value = 1 - Math.pow(1 - t, 4);
179
+ const k = 1 - Math.exp(-dt / 0.6);
180
+ mouse.x += ((o.interactive ? mouse.tx : 0) - mouse.x) * k;
181
+ mouse.y += ((o.interactive ? mouse.ty : 0) - mouse.y) * k;
182
+ u.uMouse.value.set(mouse.x, mouse.y);
183
+ u.uTime.value = time;
184
+ u.uSky.value.set(o.sky);
185
+ u.uMid.value.set(o.mid);
186
+ u.uAccent.value.set(o.accent);
187
+ u.uCore.value.set(o.core);
188
+ u.uSpread.value = o.spread;
189
+ u.uIntensity.value = o.intensity;
190
+ u.uShimmer.value = reduced ? 0 : o.shimmer;
191
+ u.uBreath.value = reduced ? 0 : o.breathe;
192
+ u.uGrain.value = o.grain;
193
+ u.uArc.value = o.arc;
194
+ u.uRays.value = reduced ? o.rays * 0.6 : o.rays;
195
+ u.uDust.value = reduced ? 0 : o.dust;
196
+ renderer.render(scene, camera);
197
+ };
198
+ const loop = (now) => {
199
+ raf = 0;
200
+ draw(now);
201
+ if (visible && !reduced) raf = requestAnimationFrame(loop);
202
+ };
203
+ const start = () => {
204
+ if (!raf && visible && !reduced) {
205
+ last = 0;
206
+ raf = requestAnimationFrame(loop);
207
+ }
208
+ };
209
+ const fit = () => {
210
+ const { width, height } = el.getBoundingClientRect();
211
+ if (!width || !height) return;
212
+ renderer.setSize(width, height, false);
213
+ renderer.getDrawingBufferSize(u.uRes.value);
214
+ if (!raf) draw(performance.now());
215
+ };
216
+
217
+ const onMove = (e) => {
218
+ const r = el.getBoundingClientRect();
219
+ mouse.tx = ((e.clientX - r.left) / r.width) * 2 - 1;
220
+ mouse.ty = -(((e.clientY - r.top) / r.height) * 2 - 1);
221
+ };
222
+ const onLeave = () => {
223
+ mouse.tx = 0;
224
+ mouse.ty = 0;
225
+ };
226
+ el.addEventListener("pointermove", onMove);
227
+ el.addEventListener("pointerleave", onLeave);
228
+ const ro = new ResizeObserver(fit);
229
+ ro.observe(el);
230
+ const io = new IntersectionObserver(([entry]) => {
231
+ visible = entry.isIntersecting;
232
+ if (visible) start();
233
+ });
234
+ io.observe(el);
235
+ fit();
236
+ start();
237
+
238
+ return () => {
239
+ cancelAnimationFrame(raf);
240
+ ro.disconnect();
241
+ io.disconnect();
242
+ el.removeEventListener("pointermove", onMove);
243
+ el.removeEventListener("pointerleave", onLeave);
244
+ geometry.dispose();
245
+ material.dispose();
246
+ renderer.dispose();
247
+ renderer.forceContextLoss();
248
+ renderer.domElement.remove();
249
+ };
250
+ }, []);
251
+
252
+ return (
253
+ <div ref={wrap} className={`relative h-full w-full overflow-hidden bg-[#0d1438] ${className}`}>
254
+ {children && <div className="relative z-10 h-full w-full">{children}</div>}
255
+ </div>
256
+ );
257
+ };
258
+
259
+ export default HorizonGlow;
@@ -0,0 +1,116 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+
3
+ /*
4
+ * Image Accordion: a row of image panels where the active one opens wide and
5
+ * the rest fold into slim strips with their titles turned sideways. Hover,
6
+ * focus or tap a panel to open it; it can also cycle on its own. Below
7
+ * 520px of width it stacks vertically and the panels grow in height instead,
8
+ * so it fits phones. The panels animate flex-grow with one easing curve, so
9
+ * the open and close always move together.
10
+ */
11
+
12
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets/componentsimage";
13
+ const DEFAULT_ITEMS = [
14
+ { image: `${IK}/galaxy-3d.png?tr=f-auto,q-80,w-1100`, title: "Galaxy 3D", description: "A turning spiral galaxy for hero sections." },
15
+ { image: `${IK}/sunrise-background.png?tr=f-auto,q-80,w-1100`, title: "Sunrise", description: "A sun rising between two mountain ranges." },
16
+ { image: `${IK}/liquid-blob-3d.png?tr=f-auto,q-80,w-1100`, title: "Liquid Blob", description: "A glossy blob that ripples toward the pointer." },
17
+ { image: `${IK}/warp-tunnel.png?tr=f-auto,q-80,w-1100`, title: "Warp Tunnel", description: "Hold to boost into hyperspace." },
18
+ { image: `${IK}/horizon-glow-v2.png?tr=f-auto,q-80,w-1100`, title: "Horizon Glow", description: "Soft light rising from below the edge." },
19
+ ];
20
+
21
+ const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
22
+
23
+ const ImageAccordion = ({ items = DEFAULT_ITEMS, defaultActive = 0, autoPlay = 0, openRatio = 4, radius = 1.25, gap = 0.6, duration = 700, height = "26rem", onChange, className = "" }) => {
24
+ const list = items.length ? items : DEFAULT_ITEMS;
25
+ const [active, setActive] = useState(Math.min(defaultActive, list.length - 1));
26
+ const [vertical, setVertical] = useState(false);
27
+ const root = useRef(null);
28
+ const hovering = useRef(false);
29
+
30
+ const open = (i) => {
31
+ setActive((cur) => {
32
+ if (cur !== i) onChange?.(i);
33
+ return i;
34
+ });
35
+ };
36
+
37
+ // Stack vertically when the box is narrow (a container check, not the viewport).
38
+ useEffect(() => {
39
+ const el = root.current;
40
+ const ro = new ResizeObserver(([entry]) => setVertical(entry.contentRect.width < 520));
41
+ ro.observe(el);
42
+ return () => ro.disconnect();
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ if (!autoPlay) return undefined;
47
+ const id = setInterval(() => {
48
+ if (!hovering.current) setActive((cur) => (cur + 1) % list.length);
49
+ }, autoPlay * 1000);
50
+ return () => clearInterval(id);
51
+ }, [autoPlay, list.length]);
52
+
53
+ return (
54
+ <div
55
+ ref={root}
56
+ role="tablist"
57
+ aria-orientation={vertical ? "vertical" : "horizontal"}
58
+ onPointerEnter={() => (hovering.current = true)}
59
+ onPointerLeave={() => (hovering.current = false)}
60
+ className={`flex w-full ${vertical ? "flex-col" : "flex-row"} ${className}`}
61
+ style={{ height: vertical ? `max(${height}, ${list.length * 3.4 + openRatio * 3}rem)` : height, gap: `${gap}rem` }}
62
+ >
63
+ {list.map((item, i) => {
64
+ const on = i === active;
65
+ return (
66
+ <button
67
+ key={i}
68
+ type="button"
69
+ role="tab"
70
+ aria-selected={on}
71
+ aria-label={item.title}
72
+ onPointerEnter={(e) => e.pointerType === "mouse" && open(i)}
73
+ onFocus={() => open(i)}
74
+ onClick={() => open(i)}
75
+ className="group relative min-h-0 min-w-0 overflow-hidden text-left text-white outline-none focus-visible:ring-2 focus-visible:ring-white/70"
76
+ style={{ flexGrow: on ? openRatio : 1, flexBasis: 0, borderRadius: `${radius}rem`, transition: `flex-grow ${duration}ms ${EASE}` }}
77
+ >
78
+ <img
79
+ src={item.image}
80
+ alt=""
81
+ draggable={false}
82
+ className="absolute inset-0 h-full w-full object-cover"
83
+ style={{ transform: on ? "scale(1)" : "scale(1.12)", filter: on ? "none" : "saturate(0.6) brightness(0.75)", transition: `transform ${duration * 1.4}ms ${EASE}, filter ${duration}ms ${EASE}` }}
84
+ />
85
+ <span aria-hidden="true" className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
86
+
87
+ {/* Folded: the title runs along the strip. */}
88
+ <span
89
+ aria-hidden="true"
90
+ className="absolute whitespace-nowrap text-sm font-semibold tracking-wide"
91
+ style={{
92
+ ...(vertical ? { left: "1rem", top: "50%", transform: "translateY(-50%)" } : { left: "50%", bottom: "1.25rem", transform: "translateX(-50%) rotate(-90deg)", transformOrigin: "center", translate: "0 -2.5rem" }),
93
+ opacity: on ? 0 : 0.9,
94
+ transition: `opacity ${duration * 0.5}ms ease`,
95
+ }}
96
+ >
97
+ {item.title}
98
+ </span>
99
+
100
+ {/* Open: title and text rise in once the panel has room. */}
101
+ <span
102
+ className="absolute inset-x-0 bottom-0 block p-[clamp(1rem,3cqw,1.75rem)]"
103
+ style={{ opacity: on ? 1 : 0, transform: on ? "none" : "translateY(0.75rem)", transition: `opacity ${duration * 0.6}ms ${EASE} ${on ? duration * 0.35 : 0}ms, transform ${duration * 0.8}ms ${EASE} ${on ? duration * 0.3 : 0}ms` }}
104
+ >
105
+ <span className="block font-mono text-[0.7rem] tracking-[0.2em] text-white/60">{String(i + 1).padStart(2, "0")}</span>
106
+ <span className="mt-1 block text-[clamp(1.2rem,2.4vw,1.9rem)] font-bold leading-tight">{item.title}</span>
107
+ {item.description && <span className="mt-1 block max-w-sm text-sm text-white/75">{item.description}</span>}
108
+ </span>
109
+ </button>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ };
115
+
116
+ export default ImageAccordion;