// waiv-gallery.jsx — ProductGallery with two explorable layouts the user can
// switch between: "Deck" (cover-flow 3D arc, drag/flick through) and "Wall"
// (magnetic tilt grid with a cursor spotlight). Both share a front/back modal
// and morph the section background to the focused card's signature colour.
(function () {
const { useState, useRef, useEffect } = React;
const PRODUCTS = [
{ id: 'sapphire', name: 'Sapphire', tag: 'Crystalline polygon blue', accent: '#3f8bff', bg1: '#0d2b57', bg2: '#03101f' },
{ id: 'cobalt', name: 'Cobalt Waves', tag: 'Deep ocean wave', accent: '#2f63e0', bg1: '#0a2350', bg2: '#030c1e' },
{ id: 'azure', name: 'Azure', tag: 'Bright sky blue', accent: '#46a6ff', bg1: '#0b3358', bg2: '#03101f' },
{ id: 'seafrost', name: 'Sea Frost', tag: 'Frosted glass blue', accent: '#8fc6d6', bg1: '#10303a', bg2: '#06141a' },
{ id: 'midnight', name: 'Midnight Gold', tag: 'Navy & gold leaf', accent: '#e7c873', bg1: '#101f3d', bg2: '#05060f' },
{ id: 'sphinxgold', name: 'Sphinx Gold', tag: 'Geometric gold lines', accent: '#d8b24a', bg1: '#16203a', bg2: '#070a12' },
{ id: 'obsidian', name: 'Obsidian', tag: 'Gold-dust eclipse', accent: '#d2ab57', bg1: '#1b160e', bg2: '#070707' },
{ id: 'sphinxsilver', name: 'Sphinx Silver', tag: 'Geometric silver', accent: '#c2cad6', bg1: '#1a2230', bg2: '#080b10' },
{ id: 'heritage', name: 'Heritage Cruiser', tag: 'Vintage motor', accent: '#67b9a4', bg1: '#143029', bg2: '#06120e' },
{ id: 'bumblebee', name: 'Bumblebee', tag: 'Electric wave yellow', accent: '#f2c200', bg1: '#2a2706', bg2: '#0c0b03' },
{ id: 'composite', name: 'Composite', tag: 'Sunset gradient mesh', accent: '#e5703f', bg1: '#3a1726', bg2: '#10070d' },
{ id: 'barbie', name: 'Barbie', tag: 'Bauhaus rose & plum', accent: '#e0577f', bg1: '#3a1430', bg2: '#10060d' },
{ id: 'ivory', name: 'Ivory', tag: 'Warm minimalist cream', accent: '#cbb9a0', bg1: '#2a2418', bg2: '#0d0b07' },
{ id: 'limelight', name: 'Limelight', tag: 'Signal-green emboss', accent: '#bcd24a', bg1: '#212d10', bg2: '#0a0d06' },
];
PRODUCTS.forEach(p => {
const R = window.__resources;
p.front = (R && R['pf_' + p.id]) || `assets/products/${p.id}.jpg`;
p.back = (R && R['pb_' + p.id]) || `assets/products/${p.id}-back.jpg`;
});
const N = PRODUCTS.length;
function ProductGallery() {
const secRef = useRef(null);
const [mode, setMode] = useState('deck');
const [focus, setFocus] = useState(0);
const [open, setOpen] = useState(null);
const [flip, setFlip] = useState(false);
const setBg = (p) => {
const el = secRef.current; if (!el || !p) return;
el.style.setProperty('--pg-bg1', p.bg1); el.style.setProperty('--pg-bg2', p.bg2); el.style.setProperty('--pg-accent', p.accent);
};
useEffect(() => { setBg(PRODUCTS[focus]); }, [focus]);
const openCard = (p) => { setFlip(false); setOpen(p); setBg(p); };
const close = () => { setOpen(null); setBg(PRODUCTS[focus]); };
useEffect(() => { const k = (e) => { if (e.key === 'Escape') close(); }; window.addEventListener('keydown', k); return () => window.removeEventListener('keydown', k); }, [focus]);
return (
THE COLLECTION
Pick a card people won’t put down.
Fourteen finishes, one secure chip. {mode === 'deck' ? 'Flick through the deck' : 'Sweep across the wall'} — tap any card to see both sides.
{mode === 'deck'
?
: }
{PRODUCTS[focus].name}
{PRODUCTS[focus].tag}
{/* shared front/back modal */}
{open && (
e.stopPropagation()}>
{open.tag.toUpperCase()}
{open.name}
Premium UV-printed finish with a secure NTAG 424 DNA chip sealed inside. Unlimited taps, no subscription to share.
Price₹899
ChipNTAG 424 DNA
TapsUnlimited
)}
);
}
/* ─────────── DECK: cover-flow 3D arc ─────────── */
function DeckView({ focus, setFocus, onOpen }) {
const stageRef = useRef(null);
const cardEls = useRef([]);
const cur = useRef(focus); // eased float index
const target = useRef(focus);
const drag = useRef(null);
const moved = useRef(false);
useEffect(() => {
let raf;
const loop = () => {
cur.current += (target.current - cur.current) * 0.12;
const vw = window.innerWidth;
const cardW = vw < 640 ? 190 : vw < 1000 ? 230 : 270;
const spacing = cardW * 0.62;
cardEls.current.forEach((el, i) => {
if (!el) return;
const o = i - cur.current; // signed offset from center
const ao = Math.abs(o);
const x = o * spacing;
const rotY = Math.max(-58, Math.min(58, -o * 26));
const z = -ao * 120;
const sc = Math.max(0.7, 1 - ao * 0.08);
el.style.width = cardW + 'px';
el.style.transform = `translate(-50%,-50%) translateX(${x.toFixed(1)}px) translateZ(${z.toFixed(1)}px) rotateY(${rotY.toFixed(1)}deg) scale(${sc.toFixed(3)})`;
el.style.zIndex = String(200 - Math.round(ao * 10));
el.style.opacity = ao > 5.5 ? '0' : (1 - ao * 0.12).toFixed(3);
el.style.filter = ao < 0.5 ? 'none' : `brightness(${(0.95 - ao * 0.12).toFixed(2)})`;
el.classList.toggle('is-center', ao < 0.5);
});
const f = Math.round(cur.current);
if (f !== focusRef.current) { focusRef.current = f; setFocus(((f % N) + N) % N); }
raf = requestAnimationFrame(loop);
};
const focusRef = { current: focus };
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
const go = (n) => { target.current = Math.max(0, Math.min(N - 1, n)); };
useEffect(() => {
const stage = stageRef.current;
const down = (e) => { drag.current = { x: e.clientX, start: target.current }; moved.current = false; stage.classList.add('is-grab'); };
const move = (e) => {
if (!drag.current) return;
const dx = e.clientX - drag.current.x;
if (Math.abs(dx) > 3) moved.current = true;
const vw = window.innerWidth, cardW = vw < 640 ? 190 : vw < 1000 ? 230 : 270;
target.current = Math.max(0, Math.min(N - 1, drag.current.start - dx / (cardW * 0.62)));
};
const up = () => { if (!drag.current) return; drag.current = null; stage.classList.remove('is-grab'); target.current = Math.max(0, Math.min(N - 1, Math.round(target.current))); };
const wheel = (e) => { if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) { e.preventDefault(); target.current = Math.max(0, Math.min(N - 1, target.current + e.deltaX * 0.01)); } };
stage.addEventListener('pointerdown', down);
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
stage.addEventListener('wheel', wheel, { passive: false });
return () => { stage.removeEventListener('pointerdown', down); window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); stage.removeEventListener('wheel', wheel); };
}, []);
const click = (i) => { if (moved.current) return; if (Math.abs(i - cur.current) < 0.5) onOpen(PRODUCTS[i]); else go(i); };
return (
{PRODUCTS.map((p, i) => (
))}
{PRODUCTS.map((p, i) => (
);
}
/* ─────────── WALL: magnetic tilt grid ─────────── */
function WallView({ setFocus, onOpen, setBg }) {
const gridRef = useRef(null);
useEffect(() => {
const grid = gridRef.current;
const cards = [...grid.querySelectorAll('.pg-wall-card')];
const onMove = (e) => {
cards.forEach((el) => {
const r = el.getBoundingClientRect();
const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
if (inside) {
const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
el.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
el.style.setProperty('--my', (py * 100).toFixed(1) + '%');
el.style.transform = `perspective(700px) rotateY(${((px - 0.5) * 18).toFixed(1)}deg) rotateX(${((0.5 - py) * 18).toFixed(1)}deg) translateZ(26px) scale(1.06)`;
}
});
};
window.addEventListener('pointermove', onMove, { passive: true });
return () => window.removeEventListener('pointermove', onMove);
}, []);
return (
{PRODUCTS.map((p, i) => (
))}
);
}
window.ProductGallery = ProductGallery;
})();