apex-ui-kit 2.7.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.
@@ -0,0 +1,207 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderer } from "three";
3
+
4
+ /*
5
+ * Dither Waves: slow flowing waves turned into crisp two-tone pixel art with
6
+ * ordered (Bayer) dithering, drawn in one WebGL shader. The pointer drops
7
+ * ripples that spread through the pattern. Pick the two colors, the pixel
8
+ * size and the matrix (4x4 is chunkier, 8x8 smoother).
9
+ */
10
+
11
+ const VERT = `void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }`;
12
+
13
+ const FRAG = `
14
+ precision highp float;
15
+ uniform vec2 uRes; uniform float uTime; uniform float uPixel; uniform float uScale; uniform float uContrast;
16
+ uniform vec3 uInk; uniform vec3 uPaper; uniform float uMatrix;
17
+ uniform vec3 uRipples[6]; // x, y in pixels, start time
18
+ uniform float uRipple; uniform vec2 uMouse; uniform float uHover;
19
+
20
+ float bayer2(vec2 a){ a = floor(a); return fract(a.x / 2.0 + a.y * a.y * 0.75); }
21
+ float bayer4(vec2 a){ return bayer2(0.5 * a) * 0.25 + bayer2(a); }
22
+ float bayer8(vec2 a){ return bayer4(0.5 * a) * 0.25 + bayer2(a); }
23
+
24
+ float h2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
25
+ float n2(vec2 p){
26
+ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);
27
+ 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);
28
+ }
29
+ float fbm(vec2 p){ float v = 0.0; float a = 0.5; for (int i = 0; i < 5; i++) { v += a * n2(p); p = p * 2.03 + 11.7; a *= 0.5; } return v; }
30
+
31
+ void main(){
32
+ // Snap to the pixel grid first so the whole field reads as chunky pixels.
33
+ vec2 cell = floor(gl_FragCoord.xy / uPixel);
34
+ vec2 px = (cell + 0.5) * uPixel;
35
+ vec2 p = px / uRes.y * uScale;
36
+ float t = uTime;
37
+
38
+ // Domain-warped waves drifting diagonally.
39
+ vec2 w = vec2(fbm(p * 0.9 + vec2(t * 0.06, -t * 0.04)), fbm(p * 0.9 + vec2(-t * 0.05, t * 0.07) + 5.2));
40
+ float v = fbm(p + w * 1.6 + vec2(t * 0.03, 0.0));
41
+ v += 0.18 * sin(p.x * 1.3 + p.y * 0.7 - t * 0.6 + w.x * 4.0);
42
+
43
+ // Ripples from the pointer.
44
+ float rip = 0.0;
45
+ for (int i = 0; i < 6; i++) {
46
+ vec3 r = uRipples[i];
47
+ float age = t - r.z;
48
+ if (r.z > 0.0 && age < 3.5) {
49
+ float dist = length(px - r.xy) / uRes.y;
50
+ float front = age * 0.45;
51
+ rip += sin((dist - front) * 38.0) * exp(-abs(dist - front) * 14.0) * exp(-age * 0.9);
52
+ }
53
+ }
54
+ v += rip * 0.22 * uRipple;
55
+ // A soft glow under the pointer.
56
+ v += uHover * 0.18 * exp(-length(px - uMouse) / uRes.y * 7.0);
57
+
58
+ v = clamp((v - 0.5) * uContrast + 0.5, 0.0, 1.0);
59
+ float threshold = uMatrix > 6.0 ? bayer8(cell) : bayer4(cell);
60
+ float on = step(threshold, v);
61
+ gl_FragColor = vec4(mix(uPaper, uInk, on), 1.0);
62
+ }`;
63
+
64
+ const DitherWaves = ({ ink = "#c8f560", paper = "#0b0d0a", pixelSize = 3, matrix = 8, scale = 2.4, contrast = 1.35, speed = 1, ripple = 1, interactive = true, className = "", children }) => {
65
+ const wrap = useRef(null);
66
+ const live = useRef({});
67
+ live.current = { ink, paper, pixelSize, matrix, scale, contrast, speed, ripple, interactive };
68
+
69
+ useEffect(() => {
70
+ const el = wrap.current;
71
+ const renderer = new WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" });
72
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
73
+ renderer.setPixelRatio(dpr);
74
+ Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block", imageRendering: "pixelated" });
75
+ el.prepend(renderer.domElement);
76
+ const scene = new Scene();
77
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
78
+ const u = {
79
+ uRes: { value: new Vector2(1, 1) },
80
+ uTime: { value: 0 },
81
+ uPixel: { value: 3 },
82
+ uScale: { value: 2.4 },
83
+ uContrast: { value: 1.35 },
84
+ uInk: { value: new Color() },
85
+ uPaper: { value: new Color() },
86
+ uMatrix: { value: 8 },
87
+ uRipples: { value: Array.from({ length: 6 }, () => new Vector3()) },
88
+ uRipple: { value: 1 },
89
+ uMouse: { value: new Vector2(-9999, -9999) },
90
+ uHover: { value: 0 },
91
+ };
92
+ const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
93
+ const geometry = new PlaneGeometry(2, 2);
94
+ scene.add(new Mesh(geometry, material));
95
+
96
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
97
+ const mouse = { x: -9999, y: -9999, hover: 0, in: false, lastDrop: 0, lx: 0, ly: 0 };
98
+ let slot = 0;
99
+ let raf = 0;
100
+ let last = 0;
101
+ let time = 1;
102
+ let visible = true;
103
+ let h = 1;
104
+
105
+ const fit = () => {
106
+ const { width, height } = el.getBoundingClientRect();
107
+ if (!width || !height) return;
108
+ h = height;
109
+ renderer.setSize(width, height, false);
110
+ u.uRes.value.set(width * dpr, height * dpr);
111
+ if (!raf) draw(performance.now());
112
+ };
113
+ const drop = (x, y) => {
114
+ const r = u.uRipples.value[slot];
115
+ r.set(x * dpr, (h - y) * dpr, time);
116
+ slot = (slot + 1) % 6;
117
+ };
118
+ const draw = (now) => {
119
+ const o = live.current;
120
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
121
+ last = now;
122
+ time += dt * o.speed;
123
+ mouse.hover += ((o.interactive && mouse.in ? 1 : 0) - mouse.hover) * (1 - Math.exp(-dt / 0.25));
124
+ u.uTime.value = time;
125
+ u.uPixel.value = Math.max(1, o.pixelSize) * dpr;
126
+ u.uScale.value = o.scale;
127
+ u.uContrast.value = o.contrast;
128
+ // The shader writes these straight to the screen, so keep them in sRGB.
129
+ u.uInk.value.set(o.ink).convertLinearToSRGB();
130
+ u.uPaper.value.set(o.paper).convertLinearToSRGB();
131
+ u.uMatrix.value = o.matrix;
132
+ u.uRipple.value = o.interactive ? o.ripple : 0;
133
+ u.uMouse.value.set(mouse.x * dpr, (h - mouse.y) * dpr);
134
+ u.uHover.value = mouse.hover;
135
+ renderer.render(scene, camera);
136
+ };
137
+ const loop = (now) => {
138
+ raf = 0;
139
+ draw(now);
140
+ if (visible && !reduced) raf = requestAnimationFrame(loop);
141
+ };
142
+ const start = () => {
143
+ if (!raf && visible && !reduced) {
144
+ last = 0;
145
+ raf = requestAnimationFrame(loop);
146
+ }
147
+ };
148
+
149
+ const local = (e) => {
150
+ const r = el.getBoundingClientRect();
151
+ return [e.clientX - r.left, e.clientY - r.top];
152
+ };
153
+ const onMove = (e) => {
154
+ [mouse.x, mouse.y] = local(e);
155
+ mouse.in = true;
156
+ // Moving drops a ripple every so often, spaced by distance.
157
+ if (live.current.interactive && Math.hypot(mouse.x - mouse.lx, mouse.y - mouse.ly) > 90 && time - mouse.lastDrop > 0.25) {
158
+ drop(mouse.x, mouse.y);
159
+ mouse.lastDrop = time;
160
+ mouse.lx = mouse.x;
161
+ mouse.ly = mouse.y;
162
+ }
163
+ };
164
+ const onDown = (e) => {
165
+ if (!live.current.interactive) return;
166
+ const [x, y] = local(e);
167
+ drop(x, y);
168
+ };
169
+ const onLeave = () => {
170
+ mouse.in = false;
171
+ };
172
+ el.addEventListener("pointermove", onMove);
173
+ el.addEventListener("pointerdown", onDown);
174
+ el.addEventListener("pointerleave", onLeave);
175
+ const ro = new ResizeObserver(fit);
176
+ ro.observe(el);
177
+ const io = new IntersectionObserver(([entry]) => {
178
+ visible = entry.isIntersecting;
179
+ if (visible) start();
180
+ });
181
+ io.observe(el);
182
+ fit();
183
+ start();
184
+
185
+ return () => {
186
+ cancelAnimationFrame(raf);
187
+ ro.disconnect();
188
+ io.disconnect();
189
+ el.removeEventListener("pointermove", onMove);
190
+ el.removeEventListener("pointerdown", onDown);
191
+ el.removeEventListener("pointerleave", onLeave);
192
+ geometry.dispose();
193
+ material.dispose();
194
+ renderer.dispose();
195
+ renderer.forceContextLoss();
196
+ renderer.domElement.remove();
197
+ };
198
+ }, []);
199
+
200
+ return (
201
+ <div ref={wrap} className={`relative h-full w-full overflow-hidden ${className}`} style={{ background: paper }}>
202
+ {children && <div className="relative z-10 h-full w-full">{children}</div>}
203
+ </div>
204
+ );
205
+ };
206
+
207
+ export default DitherWaves;
@@ -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;
@@ -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-2.5 py-2 text-[0.82rem] font-medium 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"}`}
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,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;