@goodz-core/sdk 0.1.0 → 0.3.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,924 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/ui/GoodZCardFocus.tsx
6
+
7
+ // src/ui/form-factors.ts
8
+ var FORM_FACTORS = {
9
+ trading_card: { key: "trading_card", label: "Trading Card", aspectRatio: [5, 7], isCircle: false },
10
+ postcard: { key: "postcard", label: "Postcard", aspectRatio: [3, 2], isCircle: false },
11
+ postcard_portrait: { key: "postcard_portrait", label: "Postcard (Portrait)", aspectRatio: [2, 3], isCircle: false },
12
+ polaroid: { key: "polaroid", label: "Polaroid", aspectRatio: [3, 4], isCircle: false },
13
+ laser_ticket: { key: "laser_ticket", label: "Laser Ticket", aspectRatio: [5, 2], isCircle: false },
14
+ badge_small: { key: "badge_small", label: "Badge S", aspectRatio: [1, 1], isCircle: true },
15
+ badge_medium: { key: "badge_medium", label: "Badge M", aspectRatio: [1, 1], isCircle: true },
16
+ badge_large: { key: "badge_large", label: "Badge L", aspectRatio: [1, 1], isCircle: true },
17
+ acrylic_stand_tall: { key: "acrylic_stand_tall", label: "Transparent Card", aspectRatio: [5, 7], isCircle: false, isTransparent: true },
18
+ acrylic_stand_chibi: { key: "acrylic_stand_chibi", label: "Transparent Card (Chibi)", aspectRatio: [3, 4], isCircle: false, isTransparent: true }
19
+ };
20
+ function getFormFactorSpec(key) {
21
+ return FORM_FACTORS[key];
22
+ }
23
+ function getAspectRatioCSS(key) {
24
+ const spec = getFormFactorSpec(key);
25
+ if (!spec) return "5 / 7";
26
+ return `${spec.aspectRatio[0]} / ${spec.aspectRatio[1]}`;
27
+ }
28
+ function getHoloTier(rarity) {
29
+ if (!rarity) return "subtle";
30
+ const r = rarity.toUpperCase();
31
+ if (["UR", "SECRET", "RAINBOW", "GOLD", "PRISMATIC"].some((k) => r.includes(k))) return "rainbow";
32
+ if (["SSR", "SR", "RARE", "HOLO", "LEGENDARY", "MYTHIC"].some((k) => r.includes(k))) return "holo";
33
+ return "subtle";
34
+ }
35
+ function getHoloGradient(tier, angle) {
36
+ switch (tier) {
37
+ case "subtle":
38
+ return `linear-gradient(${angle}deg, transparent 0%, rgba(255,255,255,0.06) 30%, rgba(255,255,255,0.14) 50%, rgba(255,255,255,0.06) 70%, transparent 100%)`;
39
+ case "holo":
40
+ return `linear-gradient(${angle}deg, transparent 0%, rgba(120,200,255,0.1) 15%, rgba(255,180,120,0.12) 30%, rgba(180,120,255,0.15) 50%, rgba(120,255,200,0.12) 70%, rgba(255,120,180,0.1) 85%, transparent 100%)`;
41
+ case "rainbow":
42
+ return `linear-gradient(${angle}deg, rgba(255,0,0,0.1) 0%, rgba(255,165,0,0.12) 14%, rgba(255,255,0,0.12) 28%, rgba(0,255,0,0.12) 42%, rgba(0,127,255,0.15) 56%, rgba(75,0,130,0.12) 70%, rgba(148,0,211,0.12) 84%, rgba(255,0,0,0.1) 100%)`;
43
+ default:
44
+ return "none";
45
+ }
46
+ }
47
+ function isBadge(ff) {
48
+ return ff === "badge_small" || ff === "badge_medium" || ff === "badge_large";
49
+ }
50
+ function isAcrylic(ff) {
51
+ return ff === "acrylic_stand_tall" || ff === "acrylic_stand_chibi";
52
+ }
53
+ function isPolaroid(ff) {
54
+ return ff === "polaroid";
55
+ }
56
+ function isLaserTicket(ff) {
57
+ return ff === "laser_ticket";
58
+ }
59
+ function isPostcard(ff) {
60
+ return ff === "postcard" || ff === "postcard_portrait";
61
+ }
62
+ function isMobileDevice() {
63
+ return typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0 || /Android|iPhone|iPad|iPod/i.test(navigator.userAgent));
64
+ }
65
+ function getBadgeContainerStyle() {
66
+ return {
67
+ borderRadius: "50%",
68
+ overflow: "hidden",
69
+ boxShadow: [
70
+ "0 0 0 4px rgba(180,180,200,0.8)",
71
+ "0 0 0 6px rgba(120,120,140,0.6)",
72
+ "inset 0 -4px 12px rgba(0,0,0,0.25)",
73
+ "inset 0 4px 8px rgba(255,255,255,0.2)",
74
+ "0 8px 24px rgba(0,0,0,0.4)"
75
+ ].join(", ")
76
+ };
77
+ }
78
+ function getBadgeOverlayStyle(tiltX, tiltY) {
79
+ const px = 50 + tiltY * 2;
80
+ const py = 40 - tiltX * 1.5;
81
+ return {
82
+ background: `radial-gradient(ellipse at ${px}% ${py}%, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0.08) 40%, transparent 70%)`,
83
+ borderRadius: "50%",
84
+ mixBlendMode: "overlay"
85
+ };
86
+ }
87
+ function getPolaroidContainerStyle() {
88
+ return {
89
+ padding: "10px 10px 40px 10px",
90
+ backgroundColor: "#fafaf8",
91
+ borderRadius: "2px",
92
+ boxShadow: [
93
+ "0 2px 8px rgba(0,0,0,0.15)",
94
+ "0 8px 24px rgba(0,0,0,0.1)",
95
+ "2px 4px 6px rgba(0,0,0,0.08)"
96
+ ].join(", "),
97
+ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E")`
98
+ };
99
+ }
100
+ function getLaserTicketOverlayStyle(angle) {
101
+ return {
102
+ background: `linear-gradient(${angle}deg,
103
+ rgba(255,0,0,0.15) 0%, rgba(255,165,0,0.18) 10%, rgba(255,255,0,0.18) 20%,
104
+ rgba(0,255,0,0.18) 30%, rgba(0,200,255,0.2) 40%, rgba(0,100,255,0.2) 50%,
105
+ rgba(75,0,130,0.18) 60%, rgba(148,0,211,0.18) 70%, rgba(255,0,100,0.15) 80%,
106
+ rgba(255,100,0,0.15) 90%, rgba(255,0,0,0.15) 100%)`,
107
+ mixBlendMode: "color-dodge",
108
+ animation: "goodz-laser-shimmer 3s linear infinite"
109
+ };
110
+ }
111
+ function getAcrylicContainerStyle() {
112
+ return {
113
+ borderRadius: "16px",
114
+ overflow: "visible",
115
+ boxShadow: [
116
+ "inset 0 0 0 1px rgba(255,255,255,0.12)",
117
+ "inset 0 1px 0 0 rgba(255,255,255,0.2)",
118
+ "0 4px 8px -2px rgba(0,0,0,0.3)",
119
+ "0 12px 24px -4px rgba(0,0,0,0.4)",
120
+ "0 25px 50px -12px rgba(0,0,0,0.5)"
121
+ ].join(", "),
122
+ background: "linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%)"
123
+ };
124
+ }
125
+ function getAcrylicOverlayStyle(tiltX, tiltY) {
126
+ const px = 50 + tiltY * 1.5;
127
+ const py = 30 - tiltX * 1.5;
128
+ const edgePx = 50 - tiltY * 3;
129
+ const edgePy = 50 + tiltX * 3;
130
+ return {
131
+ background: [
132
+ `radial-gradient(ellipse at ${px}% ${py}%, rgba(255,255,255,0.2) 0%, transparent 50%)`,
133
+ `radial-gradient(ellipse at ${edgePx}% ${edgePy}%, rgba(200,220,255,0.08) 0%, transparent 40%)`,
134
+ `linear-gradient(${(Math.atan2(tiltY, tiltX) * 180 / Math.PI + 360) % 360}deg, transparent 30%, rgba(255,200,200,0.04) 40%, rgba(200,255,200,0.04) 50%, rgba(200,200,255,0.04) 60%, transparent 70%)`
135
+ ].join(", "),
136
+ mixBlendMode: "screen",
137
+ borderRadius: "16px",
138
+ boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.08)"
139
+ };
140
+ }
141
+ function getPostcardOverlayStyle() {
142
+ return {
143
+ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E")`,
144
+ mixBlendMode: "multiply"
145
+ };
146
+ }
147
+ var KEYFRAMES_ID = "__goodz-card-focus-keyframes__";
148
+ function injectKeyframes() {
149
+ if (typeof document === "undefined") return;
150
+ if (document.getElementById(KEYFRAMES_ID)) return;
151
+ const style = document.createElement("style");
152
+ style.id = KEYFRAMES_ID;
153
+ style.textContent = `
154
+ @keyframes goodz-focus-shimmer {
155
+ 0% { transform: translateX(-100%); }
156
+ 100% { transform: translateX(100%); }
157
+ }
158
+ @keyframes goodz-focus-pulse {
159
+ 0%, 100% { opacity: 0.4; }
160
+ 50% { opacity: 1; }
161
+ }
162
+ @keyframes goodz-laser-shimmer {
163
+ 0% { filter: hue-rotate(0deg) brightness(1); }
164
+ 50% { filter: hue-rotate(30deg) brightness(1.1); }
165
+ 100% { filter: hue-rotate(0deg) brightness(1); }
166
+ }
167
+ `;
168
+ document.head.appendChild(style);
169
+ }
170
+ function GoodZCardFocus({
171
+ children,
172
+ imageUrl,
173
+ cardName = "GoodZ",
174
+ rarity,
175
+ formFactor,
176
+ className = "",
177
+ disabled = false
178
+ }) {
179
+ const [isOpen, setIsOpen] = useState(false);
180
+ const [isClosing, setIsClosing] = useState(false);
181
+ const handleOpen = useCallback(() => {
182
+ if (!disabled) {
183
+ setIsClosing(false);
184
+ setIsOpen(true);
185
+ }
186
+ }, [disabled]);
187
+ const handleClose = useCallback(() => {
188
+ setIsClosing(true);
189
+ }, []);
190
+ const handleExitDone = useCallback(() => {
191
+ setIsOpen(false);
192
+ setIsClosing(false);
193
+ }, []);
194
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
195
+ /* @__PURE__ */ jsx(
196
+ "div",
197
+ {
198
+ className: className || void 0,
199
+ style: {
200
+ cursor: disabled ? "default" : "pointer",
201
+ transition: "transform 0.3s",
202
+ ...!className ? {} : {}
203
+ },
204
+ onClick: handleOpen,
205
+ role: "button",
206
+ tabIndex: 0,
207
+ onKeyDown: (e) => {
208
+ if (e.key === "Enter" || e.key === " ") handleOpen();
209
+ },
210
+ "aria-label": `View ${cardName} in detail`,
211
+ children
212
+ }
213
+ ),
214
+ isOpen && createPortal(
215
+ /* @__PURE__ */ jsx(
216
+ FocusOverlay,
217
+ {
218
+ imageUrl,
219
+ cardName,
220
+ rarity,
221
+ formFactor,
222
+ onClose: handleClose,
223
+ closing: isClosing,
224
+ onExitDone: handleExitDone
225
+ }
226
+ ),
227
+ document.body
228
+ )
229
+ ] });
230
+ }
231
+ function FocusOverlay({ imageUrl, cardName, rarity, formFactor, onClose, closing, onExitDone }) {
232
+ const overlayRef = useRef(null);
233
+ const cardRef = useRef(null);
234
+ const holoRef = useRef(null);
235
+ const sparkleRef = useRef(null);
236
+ const materialRef = useRef(null);
237
+ const [visible, setVisible] = useState(false);
238
+ const [imageLoaded, setImageLoaded] = useState(false);
239
+ const [gyroStatus, setGyroStatus] = useState("checking");
240
+ const [useGyro, setUseGyro] = useState(true);
241
+ const [showHint, setShowHint] = useState(true);
242
+ const tier = getHoloTier(rarity);
243
+ const isMobile = useRef(isMobileDevice()).current;
244
+ const tiltRef = useRef({ x: 0, y: 0 });
245
+ const targetTiltRef = useRef({ x: 0, y: 0 });
246
+ const gyroListenerRef = useRef(null);
247
+ const touchStartRef = useRef(null);
248
+ const lerpRafRef = useRef(null);
249
+ const rafRef = useRef(null);
250
+ const entryDoneRef = useRef(false);
251
+ const lerpActiveRef = useRef(false);
252
+ const ff = formFactor || "trading_card";
253
+ const useBadgeMaterial = isBadge(ff);
254
+ const useAcrylicMaterial = isAcrylic(ff);
255
+ const usePolaroidMaterial = isPolaroid(ff);
256
+ const useLaserMaterial = isLaserTicket(ff);
257
+ const usePostcardMaterial = isPostcard(ff);
258
+ const useDefaultHolo = !useBadgeMaterial && !useAcrylicMaterial && !usePolaroidMaterial && !useLaserMaterial && !usePostcardMaterial;
259
+ useEffect(() => {
260
+ injectKeyframes();
261
+ }, []);
262
+ useEffect(() => {
263
+ const scrollY = window.scrollY;
264
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
265
+ const prevOverflow = document.body.style.overflow;
266
+ const prevPosition = document.body.style.position;
267
+ const prevTop = document.body.style.top;
268
+ const prevWidth = document.body.style.width;
269
+ const prevPaddingRight = document.body.style.paddingRight;
270
+ document.body.style.overflow = "hidden";
271
+ document.body.style.position = "fixed";
272
+ document.body.style.top = `-${scrollY}px`;
273
+ document.body.style.width = "100%";
274
+ if (scrollbarWidth > 0) {
275
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
276
+ }
277
+ setVisible(true);
278
+ const hintTimer = setTimeout(() => setShowHint(false), 3e3);
279
+ const entryTimer = setTimeout(() => {
280
+ entryDoneRef.current = true;
281
+ if (cardRef.current) {
282
+ cardRef.current.style.transition = "none";
283
+ }
284
+ }, 500);
285
+ return () => {
286
+ document.body.style.overflow = prevOverflow;
287
+ document.body.style.position = prevPosition;
288
+ document.body.style.top = prevTop;
289
+ document.body.style.width = prevWidth;
290
+ document.body.style.paddingRight = prevPaddingRight;
291
+ window.scrollTo(0, scrollY);
292
+ clearTimeout(hintTimer);
293
+ clearTimeout(entryTimer);
294
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
295
+ };
296
+ }, []);
297
+ useEffect(() => {
298
+ if (!closing) return;
299
+ setVisible(false);
300
+ if (holoRef.current) {
301
+ holoRef.current.style.transition = "opacity 0.08s ease";
302
+ holoRef.current.style.opacity = "0";
303
+ }
304
+ if (sparkleRef.current) {
305
+ sparkleRef.current.style.transition = "opacity 0.08s ease";
306
+ sparkleRef.current.style.opacity = "0";
307
+ }
308
+ if (materialRef.current) {
309
+ materialRef.current.style.transition = "opacity 0.08s ease";
310
+ materialRef.current.style.opacity = "0";
311
+ }
312
+ const cardTimer = setTimeout(() => {
313
+ if (cardRef.current) {
314
+ cardRef.current.style.transition = "opacity 0.2s ease, transform 0.25s cubic-bezier(.4,0,.2,1)";
315
+ cardRef.current.style.opacity = "0";
316
+ cardRef.current.style.transform = "perspective(800px) rotateX(0deg) rotateY(0deg) scale(0.85)";
317
+ }
318
+ }, 80);
319
+ const bgTimer = setTimeout(() => {
320
+ if (overlayRef.current) {
321
+ overlayRef.current.style.transition = "background-color 0.25s ease, backdrop-filter 0.25s ease, -webkit-backdrop-filter 0.25s ease";
322
+ overlayRef.current.style.backgroundColor = "rgba(0,0,0,0)";
323
+ overlayRef.current.style.backdropFilter = "blur(0px)";
324
+ overlayRef.current.style.WebkitBackdropFilter = "blur(0px)";
325
+ }
326
+ }, 120);
327
+ const exitTimer = setTimeout(() => {
328
+ onExitDone?.();
329
+ }, 400);
330
+ return () => {
331
+ clearTimeout(cardTimer);
332
+ clearTimeout(bgTimer);
333
+ clearTimeout(exitTimer);
334
+ };
335
+ }, [closing, onExitDone]);
336
+ useEffect(() => {
337
+ const overlay = overlayRef.current;
338
+ if (!overlay) return;
339
+ const preventScroll = (e) => {
340
+ const target = e.target;
341
+ if (target === overlay || target.closest("[data-overlay-bg]")) {
342
+ e.preventDefault();
343
+ }
344
+ };
345
+ overlay.addEventListener("touchmove", preventScroll, { passive: false });
346
+ return () => {
347
+ overlay.removeEventListener("touchmove", preventScroll);
348
+ };
349
+ }, []);
350
+ useEffect(() => {
351
+ const handler = (e) => {
352
+ if (e.key === "Escape") onClose();
353
+ };
354
+ window.addEventListener("keydown", handler);
355
+ return () => window.removeEventListener("keydown", handler);
356
+ }, [onClose]);
357
+ const updateMaterialOverlay = useCallback((tiltX, tiltY) => {
358
+ if (!materialRef.current) return;
359
+ if (useBadgeMaterial) {
360
+ materialRef.current.style.background = getBadgeOverlayStyle(tiltX, tiltY).background;
361
+ } else if (useAcrylicMaterial) {
362
+ materialRef.current.style.background = getAcrylicOverlayStyle(tiltX, tiltY).background;
363
+ }
364
+ }, [useBadgeMaterial, useAcrylicMaterial]);
365
+ const applyTilt = useCallback((tiltX, tiltY) => {
366
+ if (!cardRef.current) return;
367
+ const maxAngle = isMobile ? 14 : 20;
368
+ const rx = Math.max(-maxAngle, Math.min(maxAngle, tiltX));
369
+ const ry = Math.max(-maxAngle, Math.min(maxAngle, tiltY));
370
+ tiltRef.current = { x: rx, y: ry };
371
+ cardRef.current.style.transform = `perspective(800px) rotateX(${rx}deg) rotateY(${ry}deg)`;
372
+ const angle = (Math.atan2(ry, rx) * 180 / Math.PI + 360) % 360;
373
+ if (holoRef.current && (useDefaultHolo || useLaserMaterial)) {
374
+ if (useLaserMaterial) {
375
+ holoRef.current.style.background = getLaserTicketOverlayStyle(angle).background;
376
+ } else {
377
+ holoRef.current.style.background = getHoloGradient(tier, angle);
378
+ }
379
+ }
380
+ if (sparkleRef.current && useDefaultHolo) {
381
+ const px = 50 + ry * 2;
382
+ const py = 50 - rx * 2;
383
+ const opacity = tier === "rainbow" ? 0.25 : tier === "holo" ? 0.18 : 0.1;
384
+ sparkleRef.current.style.background = `radial-gradient(circle at ${px}% ${py}%, rgba(255,255,255,${opacity}) 0%, transparent 60%)`;
385
+ }
386
+ updateMaterialOverlay(rx, ry);
387
+ }, [isMobile, tier, useDefaultHolo, useLaserMaterial, updateMaterialOverlay]);
388
+ const scheduleTilt = useCallback((tiltX, tiltY) => {
389
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
390
+ rafRef.current = requestAnimationFrame(() => {
391
+ applyTilt(tiltX, tiltY);
392
+ rafRef.current = null;
393
+ });
394
+ }, [applyTilt]);
395
+ const LERP_FACTOR = 0.08;
396
+ const LERP_THRESHOLD = 0.01;
397
+ const startLerpLoop = useCallback(() => {
398
+ if (lerpActiveRef.current) return;
399
+ lerpActiveRef.current = true;
400
+ const loop = () => {
401
+ const cur = tiltRef.current;
402
+ const tgt = targetTiltRef.current;
403
+ const dx = tgt.x - cur.x;
404
+ const dy = tgt.y - cur.y;
405
+ if (Math.abs(dx) < LERP_THRESHOLD && Math.abs(dy) < LERP_THRESHOLD) {
406
+ if (cur.x !== tgt.x || cur.y !== tgt.y) {
407
+ applyTilt(tgt.x, tgt.y);
408
+ }
409
+ lerpActiveRef.current = false;
410
+ lerpRafRef.current = null;
411
+ return;
412
+ }
413
+ const nx = cur.x + dx * LERP_FACTOR;
414
+ const ny = cur.y + dy * LERP_FACTOR;
415
+ applyTilt(nx, ny);
416
+ lerpRafRef.current = requestAnimationFrame(loop);
417
+ };
418
+ lerpRafRef.current = requestAnimationFrame(loop);
419
+ }, [applyTilt]);
420
+ useEffect(() => {
421
+ return () => {
422
+ if (lerpRafRef.current) cancelAnimationFrame(lerpRafRef.current);
423
+ };
424
+ }, []);
425
+ const resetTilt = useCallback((_animate = false) => {
426
+ if (rafRef.current) {
427
+ cancelAnimationFrame(rafRef.current);
428
+ rafRef.current = null;
429
+ }
430
+ targetTiltRef.current = { x: 0, y: 0 };
431
+ startLerpLoop();
432
+ }, [startLerpLoop]);
433
+ const startGyro = useCallback(() => {
434
+ let baseBeta = null;
435
+ let baseGamma = null;
436
+ const sensitivity = 0.7;
437
+ const handler = (e) => {
438
+ if (e.beta === null || e.gamma === null) return;
439
+ if (baseBeta === null) {
440
+ baseBeta = e.beta;
441
+ baseGamma = e.gamma;
442
+ }
443
+ const tiltX = -((e.beta - baseBeta) * sensitivity);
444
+ const tiltY = (e.gamma - (baseGamma ?? 0)) * sensitivity;
445
+ scheduleTilt(tiltX, tiltY);
446
+ };
447
+ window.addEventListener("deviceorientation", handler);
448
+ gyroListenerRef.current = handler;
449
+ setGyroStatus("available");
450
+ setUseGyro(true);
451
+ }, [scheduleTilt]);
452
+ useEffect(() => {
453
+ if (typeof DeviceOrientationEvent === "undefined") {
454
+ setGyroStatus("unavailable");
455
+ return;
456
+ }
457
+ if (typeof DeviceOrientationEvent.requestPermission === "function") {
458
+ setGyroStatus("needs-permission");
459
+ return;
460
+ }
461
+ let testHandler = null;
462
+ const testTimeout = setTimeout(() => {
463
+ if (testHandler) window.removeEventListener("deviceorientation", testHandler);
464
+ setGyroStatus("unavailable");
465
+ }, 1500);
466
+ testHandler = (e) => {
467
+ clearTimeout(testTimeout);
468
+ if (testHandler) window.removeEventListener("deviceorientation", testHandler);
469
+ if (e.beta !== null && e.gamma !== null) {
470
+ startGyro();
471
+ } else {
472
+ setGyroStatus("unavailable");
473
+ }
474
+ };
475
+ window.addEventListener("deviceorientation", testHandler);
476
+ return () => {
477
+ clearTimeout(testTimeout);
478
+ if (testHandler) window.removeEventListener("deviceorientation", testHandler);
479
+ if (gyroListenerRef.current) window.removeEventListener("deviceorientation", gyroListenerRef.current);
480
+ };
481
+ }, [startGyro]);
482
+ const requestGyroPermission = useCallback(async () => {
483
+ try {
484
+ const state = await DeviceOrientationEvent.requestPermission();
485
+ if (state === "granted") {
486
+ startGyro();
487
+ } else {
488
+ setGyroStatus("unavailable");
489
+ }
490
+ } catch {
491
+ setGyroStatus("unavailable");
492
+ }
493
+ }, [startGyro]);
494
+ const handleMouseMove = useCallback((e) => {
495
+ if (gyroStatus === "available" || isMobile) return;
496
+ const rect = cardRef.current?.getBoundingClientRect();
497
+ if (!rect) return;
498
+ const cx = rect.left + rect.width / 2;
499
+ const cy = rect.top + rect.height / 2;
500
+ const tiltY = (e.clientX - cx) / (rect.width / 2) * 15;
501
+ const tiltX = -((e.clientY - cy) / (rect.height / 2)) * 15;
502
+ targetTiltRef.current = { x: tiltX, y: tiltY };
503
+ startLerpLoop();
504
+ }, [gyroStatus, isMobile, startLerpLoop]);
505
+ const handleMouseLeave = useCallback(() => {
506
+ if (gyroStatus === "available" || isMobile) return;
507
+ targetTiltRef.current = { x: 0, y: 0 };
508
+ startLerpLoop();
509
+ }, [gyroStatus, isMobile, startLerpLoop]);
510
+ useEffect(() => {
511
+ const card = cardRef.current;
512
+ if (!card || !isMobile || gyroStatus === "available" && useGyro) return;
513
+ const SENSITIVITY = 0.4;
514
+ const onTouchStart = (e) => {
515
+ const touch = e.touches[0];
516
+ touchStartRef.current = { x: touch.clientX, y: touch.clientY };
517
+ };
518
+ const onTouchMove = (e) => {
519
+ if (!touchStartRef.current) return;
520
+ e.preventDefault();
521
+ const touch = e.touches[0];
522
+ const dx = touch.clientX - touchStartRef.current.x;
523
+ const dy = touch.clientY - touchStartRef.current.y;
524
+ const tiltY = dx * SENSITIVITY;
525
+ const tiltX = -(dy * SENSITIVITY);
526
+ scheduleTilt(tiltX, tiltY);
527
+ };
528
+ const onTouchEnd = () => {
529
+ touchStartRef.current = null;
530
+ resetTilt(true);
531
+ };
532
+ card.addEventListener("touchstart", onTouchStart, { passive: true });
533
+ card.addEventListener("touchmove", onTouchMove, { passive: false });
534
+ card.addEventListener("touchend", onTouchEnd, { passive: true });
535
+ return () => {
536
+ card.removeEventListener("touchstart", onTouchStart);
537
+ card.removeEventListener("touchmove", onTouchMove);
538
+ card.removeEventListener("touchend", onTouchEnd);
539
+ };
540
+ }, [scheduleTilt, gyroStatus, useGyro, isMobile, resetTilt]);
541
+ const glareOpacity = tier === "rainbow" ? 0.4 : tier === "holo" ? 0.3 : 0.2;
542
+ const toggleInputMode = useCallback(() => {
543
+ if (gyroStatus !== "available") return;
544
+ const next = !useGyro;
545
+ setUseGyro(next);
546
+ if (!next) {
547
+ if (gyroListenerRef.current) {
548
+ window.removeEventListener("deviceorientation", gyroListenerRef.current);
549
+ gyroListenerRef.current = null;
550
+ }
551
+ resetTilt(true);
552
+ } else {
553
+ startGyro();
554
+ }
555
+ }, [gyroStatus, useGyro, resetTilt, startGyro]);
556
+ const hintText = gyroStatus === "available" && useGyro ? "\u{1F4F1} Tilt your phone to rotate the card" : gyroStatus === "needs-permission" ? "\u{1F4F1} Tap the button below to enable gyroscope" : isMobile ? "\u{1F446} Touch & drag the card to tilt" : "\u2728 Move to tilt the card";
557
+ const cardWidth = isBadge(ff) ? "min(70vw, 320px)" : isLaserTicket(ff) ? "min(90vw, 500px)" : isPostcard(ff) ? "min(85vw, 460px)" : "min(80vw, 400px)";
558
+ const imageContainerStyle = useBadgeMaterial ? getBadgeContainerStyle() : usePolaroidMaterial ? getPolaroidContainerStyle() : useAcrylicMaterial ? getAcrylicContainerStyle() : {
559
+ borderRadius: "16px",
560
+ overflow: "hidden",
561
+ boxShadow: "0 25px 50px -12px rgba(0,0,0,0.5)"
562
+ };
563
+ const imageBorderRadius = useBadgeMaterial ? "50%" : usePolaroidMaterial ? "0" : "16px";
564
+ const overlayBorderRadius = useBadgeMaterial ? "50%" : usePolaroidMaterial ? "2px" : "16px";
565
+ const overlayStyle = {
566
+ position: "fixed",
567
+ inset: 0,
568
+ zIndex: 9999,
569
+ display: "flex",
570
+ alignItems: "center",
571
+ justifyContent: "center",
572
+ backgroundColor: "rgba(0,0,0,0.85)",
573
+ backdropFilter: visible ? "blur(12px)" : "blur(0px)",
574
+ WebkitBackdropFilter: visible ? "blur(12px)" : "blur(0px)",
575
+ transition: "backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease",
576
+ touchAction: "none",
577
+ overscrollBehavior: "none"
578
+ };
579
+ const closeButtonStyle = {
580
+ position: "absolute",
581
+ top: "16px",
582
+ right: "16px",
583
+ width: "48px",
584
+ height: "48px",
585
+ borderRadius: "50%",
586
+ background: "rgba(255,255,255,0.1)",
587
+ border: "none",
588
+ display: "flex",
589
+ alignItems: "center",
590
+ justifyContent: "center",
591
+ cursor: "pointer",
592
+ zIndex: 1e4,
593
+ opacity: closing ? 0 : 1,
594
+ transition: closing ? "opacity 0.1s ease" : "opacity 0.3s ease",
595
+ pointerEvents: closing ? "none" : "auto",
596
+ touchAction: "manipulation"
597
+ };
598
+ const toggleButtonStyle = {
599
+ position: "absolute",
600
+ bottom: "80px",
601
+ left: "50%",
602
+ transform: "translateX(-50%)",
603
+ padding: "10px 20px",
604
+ borderRadius: "9999px",
605
+ background: "rgba(255,255,255,0.15)",
606
+ border: "none",
607
+ color: "white",
608
+ fontSize: "14px",
609
+ fontWeight: 500,
610
+ cursor: "pointer",
611
+ zIndex: 1e4,
612
+ opacity: closing ? 0 : 1,
613
+ transition: closing ? "opacity 0.1s ease" : "opacity 0.3s ease",
614
+ pointerEvents: closing ? "none" : "auto",
615
+ touchAction: "manipulation"
616
+ };
617
+ return /* @__PURE__ */ jsxs(
618
+ "div",
619
+ {
620
+ ref: overlayRef,
621
+ "data-overlay-bg": true,
622
+ style: overlayStyle,
623
+ onClick: (e) => {
624
+ const target = e.target;
625
+ if (target.closest("button") || target.closest("a")) return;
626
+ onClose();
627
+ },
628
+ onMouseMove: handleMouseMove,
629
+ children: [
630
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: "12px" }, children: [
631
+ imageUrl && !imageLoaded && visible && /* @__PURE__ */ jsx(
632
+ "div",
633
+ {
634
+ style: {
635
+ position: "absolute",
636
+ inset: 0,
637
+ display: "flex",
638
+ alignItems: "center",
639
+ justifyContent: "center",
640
+ pointerEvents: "none",
641
+ zIndex: 10
642
+ },
643
+ children: /* @__PURE__ */ jsxs(
644
+ "div",
645
+ {
646
+ style: {
647
+ width: cardWidth,
648
+ aspectRatio: getAspectRatioCSS(ff),
649
+ borderRadius: useBadgeMaterial ? "50%" : usePolaroidMaterial ? "4px" : "16px",
650
+ overflow: "hidden",
651
+ position: "relative",
652
+ ...usePolaroidMaterial ? { padding: "10px 10px 40px 10px", backgroundColor: "rgba(250,250,248,0.08)" } : {}
653
+ },
654
+ children: [
655
+ /* @__PURE__ */ jsx(
656
+ "div",
657
+ {
658
+ style: {
659
+ position: "absolute",
660
+ inset: 0,
661
+ background: "linear-gradient(135deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.03) 100%)",
662
+ animation: "goodz-focus-pulse 2s ease-in-out infinite"
663
+ }
664
+ }
665
+ ),
666
+ /* @__PURE__ */ jsx(
667
+ "div",
668
+ {
669
+ style: {
670
+ position: "absolute",
671
+ inset: 0,
672
+ background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.08) 40%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.08) 60%, transparent 100%)",
673
+ animation: "goodz-focus-shimmer 2s ease-in-out infinite"
674
+ }
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsx(
678
+ "div",
679
+ {
680
+ style: {
681
+ position: "absolute",
682
+ inset: 0,
683
+ borderRadius: "inherit",
684
+ border: "1px solid rgba(255,255,255,0.08)"
685
+ }
686
+ }
687
+ )
688
+ ]
689
+ }
690
+ )
691
+ }
692
+ ),
693
+ /* @__PURE__ */ jsxs(
694
+ "div",
695
+ {
696
+ ref: cardRef,
697
+ style: {
698
+ position: "relative",
699
+ userSelect: "none",
700
+ width: cardWidth,
701
+ transform: visible ? "perspective(800px) rotateX(0deg) rotateY(0deg)" : "perspective(800px) rotateX(0deg) rotateY(0deg) scale(0.8)",
702
+ opacity: visible && (!imageUrl || imageLoaded) ? 1 : 0,
703
+ transition: "opacity 0.4s cubic-bezier(.16,1,.3,1), transform 0.15s ease-out",
704
+ transformOrigin: "center center",
705
+ transformStyle: "preserve-3d",
706
+ willChange: "transform",
707
+ touchAction: "none"
708
+ },
709
+ onMouseLeave: handleMouseLeave,
710
+ children: [
711
+ /* @__PURE__ */ jsx("div", { style: { ...imageContainerStyle, position: "relative" }, children: imageUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
712
+ !imageLoaded && /* @__PURE__ */ jsx(
713
+ "div",
714
+ {
715
+ style: {
716
+ position: "absolute",
717
+ inset: 0,
718
+ overflow: "hidden",
719
+ borderRadius: usePolaroidMaterial ? "0" : imageBorderRadius,
720
+ background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #1a1a2e 100%)"
721
+ },
722
+ children: /* @__PURE__ */ jsx(
723
+ "div",
724
+ {
725
+ style: {
726
+ position: "absolute",
727
+ inset: 0,
728
+ background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 40%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.06) 60%, transparent 100%)",
729
+ animation: "goodz-focus-shimmer 1.8s ease-in-out infinite"
730
+ }
731
+ }
732
+ )
733
+ }
734
+ ),
735
+ /* @__PURE__ */ jsx(
736
+ "img",
737
+ {
738
+ src: imageUrl,
739
+ alt: cardName,
740
+ style: {
741
+ width: "100%",
742
+ display: "block",
743
+ borderRadius: usePolaroidMaterial ? "0" : imageBorderRadius,
744
+ aspectRatio: getAspectRatioCSS(ff),
745
+ objectFit: "cover",
746
+ opacity: imageLoaded ? 1 : 0,
747
+ transition: "opacity 0.3s ease"
748
+ },
749
+ draggable: false,
750
+ onLoad: () => setImageLoaded(true)
751
+ }
752
+ )
753
+ ] }) : /* @__PURE__ */ jsx(
754
+ "div",
755
+ {
756
+ style: {
757
+ width: "100%",
758
+ background: "#262626",
759
+ display: "flex",
760
+ alignItems: "center",
761
+ justifyContent: "center",
762
+ color: "#737373",
763
+ aspectRatio: getAspectRatioCSS(ff),
764
+ borderRadius: imageBorderRadius
765
+ },
766
+ children: /* @__PURE__ */ jsx("svg", { style: { width: "64px", height: "64px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx(
767
+ "path",
768
+ {
769
+ strokeLinecap: "round",
770
+ strokeLinejoin: "round",
771
+ strokeWidth: 1.5,
772
+ d: "M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
773
+ }
774
+ ) })
775
+ }
776
+ ) }),
777
+ /* @__PURE__ */ jsx(
778
+ "div",
779
+ {
780
+ style: {
781
+ position: "absolute",
782
+ inset: 0,
783
+ pointerEvents: "none",
784
+ background: useBadgeMaterial ? `radial-gradient(circle at 50% 30%, rgba(255,255,255,${glareOpacity + 0.1}) 0%, transparent 60%)` : `radial-gradient(circle at 50% 30%, rgba(255,255,255,${glareOpacity}) 0%, transparent 70%)`,
785
+ mixBlendMode: "overlay",
786
+ borderRadius: overlayBorderRadius,
787
+ ...usePolaroidMaterial ? { top: "10px", left: "10px", right: "10px", bottom: "40px" } : {}
788
+ }
789
+ }
790
+ ),
791
+ (useDefaultHolo || useLaserMaterial) && /* @__PURE__ */ jsx(
792
+ "div",
793
+ {
794
+ ref: holoRef,
795
+ style: {
796
+ position: "absolute",
797
+ inset: 0,
798
+ pointerEvents: "none",
799
+ background: useLaserMaterial ? getLaserTicketOverlayStyle(125).background : getHoloGradient(tier, 125),
800
+ mixBlendMode: useLaserMaterial ? "color-dodge" : tier === "subtle" ? "overlay" : "color-dodge",
801
+ borderRadius: overlayBorderRadius,
802
+ ...useLaserMaterial ? { animation: "goodz-laser-shimmer 3s linear infinite" } : {}
803
+ }
804
+ }
805
+ ),
806
+ useDefaultHolo && (tier === "holo" || tier === "rainbow") && /* @__PURE__ */ jsx(
807
+ "div",
808
+ {
809
+ ref: sparkleRef,
810
+ style: {
811
+ position: "absolute",
812
+ inset: 0,
813
+ pointerEvents: "none",
814
+ background: "none",
815
+ mixBlendMode: "overlay",
816
+ borderRadius: overlayBorderRadius
817
+ }
818
+ }
819
+ ),
820
+ (useBadgeMaterial || useAcrylicMaterial || usePostcardMaterial) && /* @__PURE__ */ jsx(
821
+ "div",
822
+ {
823
+ ref: materialRef,
824
+ style: {
825
+ position: "absolute",
826
+ inset: 0,
827
+ pointerEvents: "none",
828
+ ...useBadgeMaterial ? getBadgeOverlayStyle(0, 0) : {},
829
+ ...useAcrylicMaterial ? getAcrylicOverlayStyle(0, 0) : {},
830
+ ...usePostcardMaterial ? getPostcardOverlayStyle() : {},
831
+ borderRadius: overlayBorderRadius
832
+ }
833
+ }
834
+ )
835
+ ]
836
+ }
837
+ ),
838
+ /* @__PURE__ */ jsx(
839
+ "p",
840
+ {
841
+ style: {
842
+ fontSize: "14px",
843
+ fontWeight: 600,
844
+ textAlign: "center",
845
+ color: "rgba(255,255,255,0.8)",
846
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.5))",
847
+ pointerEvents: "none",
848
+ margin: 0,
849
+ opacity: visible && !closing ? 1 : 0,
850
+ transition: closing ? "opacity 0.1s ease" : "opacity 0.5s ease 0.3s",
851
+ fontFamily: usePolaroidMaterial ? "'Georgia', serif" : "inherit"
852
+ },
853
+ children: cardName
854
+ }
855
+ )
856
+ ] }),
857
+ (gyroStatus === "needs-permission" || gyroStatus === "available") && /* @__PURE__ */ jsx(
858
+ "button",
859
+ {
860
+ style: toggleButtonStyle,
861
+ onClick: (e) => {
862
+ if (isMobile) return;
863
+ if (gyroStatus === "needs-permission") {
864
+ requestGyroPermission();
865
+ } else {
866
+ toggleInputMode();
867
+ }
868
+ },
869
+ onTouchEnd: (e) => {
870
+ e.preventDefault();
871
+ e.stopPropagation();
872
+ if (gyroStatus === "needs-permission") {
873
+ requestGyroPermission();
874
+ } else {
875
+ toggleInputMode();
876
+ }
877
+ },
878
+ children: gyroStatus === "needs-permission" ? "\u{1F4F1} Enable Gyroscope" : useGyro ? "\u{1F446} Switch to Touch" : "\u{1F4F1} Switch to Gyroscope"
879
+ }
880
+ ),
881
+ showHint && /* @__PURE__ */ jsxs(
882
+ "div",
883
+ {
884
+ style: {
885
+ position: "absolute",
886
+ bottom: "32px",
887
+ left: 0,
888
+ right: 0,
889
+ textAlign: "center",
890
+ pointerEvents: "none",
891
+ opacity: visible && !closing ? 0.7 : 0,
892
+ transition: closing ? "opacity 0.1s ease" : "opacity 0.5s ease 0.5s"
893
+ },
894
+ children: [
895
+ /* @__PURE__ */ jsx("p", { style: { color: "rgba(255,255,255,0.7)", fontSize: "12px", margin: 0 }, children: hintText }),
896
+ /* @__PURE__ */ jsx("p", { style: { color: "rgba(255,255,255,0.4)", fontSize: "10px", marginTop: "4px" }, children: "Tap anywhere to close" })
897
+ ]
898
+ }
899
+ ),
900
+ /* @__PURE__ */ jsx(
901
+ "button",
902
+ {
903
+ style: closeButtonStyle,
904
+ onClick: (e) => {
905
+ if (isMobile) return;
906
+ onClose();
907
+ },
908
+ onTouchEnd: (e) => {
909
+ e.preventDefault();
910
+ e.stopPropagation();
911
+ onClose();
912
+ },
913
+ "aria-label": "Close",
914
+ children: /* @__PURE__ */ jsx("svg", { style: { width: "24px", height: "24px", color: "white" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
915
+ }
916
+ )
917
+ ]
918
+ }
919
+ );
920
+ }
921
+
922
+ export { FORM_FACTORS, GoodZCardFocus, getAspectRatioCSS, getFormFactorSpec };
923
+ //# sourceMappingURL=chunk-K6IFJWLB.js.map
924
+ //# sourceMappingURL=chunk-K6IFJWLB.js.map