/* Brainstorm X β Name Nebula. A living 2.5D semantic naming space.
Gravity (votes β mass/position), keyword wells, constellations, animated
waves, drag-to-evolve, fuse, simulated presence, sprint mode. */
(function () {
const { useState, useRef, useEffect, useMemo } = React;
const DS = window.BrainstormXDesignSystem_f2a458;
const { Button, NameCard, KeywordOrb, Badge, Avatar } = DS;
const I = window.BXIcons;
const N = window.BXNebula;
const { clamp, hash, deriveVoters, relatedIds, FireFX, FrostFX, Stars, VoterAvatars,
Header, ZoomControl, Welcome, ActivityFeed, WaveBanner, SprintBar, Portals,
InviteModal, AddCustomModal, DetailPanel } = N;
const MASS = { on_fire: 0.92, hot: 0.82, warm: 0.75, neutral: 0.7, cold: 0.64, frozen: 0.56 };
const HEAT_C = { on_fire: 'var(--heat-fire)', hot: 'var(--heat-hot)', warm: 'var(--heat-warm)', neutral: 'var(--neutral-state)', cold: 'var(--cold-cool)', frozen: 'var(--cold-frozen)' };
const HEAT_EMOJI = { on_fire: 'π₯', hot: 'π₯', cold: 'βοΈ', frozen: 'βοΈ' };
const heatFromVotes = window.BXheatFromVotes;
const scoreOf = (c, sl) => (c.up || 0) * 2 - (c.down || 0) * 2 + (sl ? 5 : 0);
const KW_POOL = ['cosy', 'vibrant', 'bold', 'handmade', 'colourful', 'cheeky', 'premium', 'local', 'bright', 'snug'];
const SEED_COUNT = (window.BXseedNames || []).length;
const NAME_POOL = [
{ id: 'pawza', name: 'Pawza', style: 'brandable', pronunciation: 'paw-zuh', reason: 'Built from your boosted "paws" + playful tone.', keywords: ['paws', 'playful'] },
{ id: 'trinkett', name: 'Trinkett', style: 'real_word', pronunciation: 'trin-ket', reason: 'Leans into quirky, collectible accessories.', keywords: ['quirky', 'cute'] },
{ id: 'bumblepaw', name: 'Bumblepaw', style: 'compound', pronunciation: 'bum-buhl-paw', reason: 'Warm and characterful β from top-voted directions.', keywords: ['playful', 'paws'] },
{ id: 'yipster', name: 'Yipster', style: 'brandable', pronunciation: 'yip-ster', reason: 'Energetic and modern, social-handle friendly.', keywords: ['fun', 'bright'] },
{ id: 'cosypaw', name: 'Cosypaw', style: 'compound', pronunciation: 'koh-zee-paw', reason: 'Soft and snug β echoes the teamβs cosy keywords.', keywords: ['cosy', 'cute'] },
{ id: 'fuzzly', name: 'Fuzzly', style: 'alternate_spelling', pronunciation: 'fuhz-lee', reason: 'Friendly and tactile, easy to say.', keywords: ['cute', 'snug'] },
];
const EVOLVE = {
shorter: { lbl: 'shorter', mk: (s) => [s.slice(0, Math.max(3, s.length - 2)), s.replace(/[aeiou]([^aeiou]*)$/i, '$1') || s + 'o'] },
premium: { lbl: 'more premium', mk: (s) => [s + 'o', s.slice(0, 1).toUpperCase() + s.slice(1) + ' & Co'] },
playful: { lbl: 'more playful', mk: (s) => [s + 'zo', 'Boop' + s.slice(0, 3)] },
unusual: { lbl: 'more unusual', mk: (s) => [s.replace(/s$/, 'x') + 'i', 'Qy' + s.slice(1)] },
british: { lbl: 'more British', mk: (s) => [s + 'shire', s + ' & Sons'] },
like: { lbl: 'more like this', mk: (s) => [s + 'a', s + 'io'] },
};
// ---- a single floating name card (position owned by the rAF loop) -----
function SceneCard({ n, motion, selected, onVote, onOpen, onDelete, onCardDown }) {
const voters = deriveVoters(n);
const mass = MASS[n.heat] || 0.84;
const [hover, setHover] = useState(false);
const expanded = hover || selected;
const heatC = HEAT_C[n.heat] || 'var(--neutral-state)';
const emoji = HEAT_EMOJI[n.heat] || '';
const sc = mass * (hover ? 1.04 : 1);
// tiny vote control for the compact node
const Mini = ({ dir }) => (
);
if (!expanded) {
// ---- compact nebula node ----
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{ position: 'relative', transform: `scale(${sc})`, transformOrigin: 'center', transition: 'transform var(--dur-base) var(--ease-spring)', cursor: 'grab' }}>
{n.heat === 'on_fire' &&
}
{n.heat === 'frozen' &&
}
{n.score > 0 ? '+' + n.score : n.score}
{n.name}
{emoji && {emoji}}
{voters.length > 0 && (
e.stopPropagation()} style={{ position: 'absolute', top: -10, right: -8, zIndex: 6 }}>
)}
);
}
// ---- expanded full card (hover / selected) ----
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{ position: 'relative', transform: `scale(${sc})`, transformOrigin: 'center', transition: 'transform var(--dur-base) var(--ease-spring)', cursor: 'grab', zIndex: 40 }}>
{n.heat === 'on_fire' &&
}
{n.heat === 'frozen' &&
}
{voters.length > 0 && (
e.stopPropagation()} style={{ position: 'absolute', left: 116, bottom: 75, zIndex: 6 }}>
)}
);
}
// ---- keyword gravity well (lives in the scene) ------------------------
function KeywordWell({ k, onBoost, onCool, onDelete }) {
const [hover, setHover] = useState(false);
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
);
}
function Arena({ names, setNames, keywords, setKeywords, wave, setWave, goto, project = 'Pickle Paws', motion = true, density = 8 }) {
const [openId, setOpenId] = useState(null);
const [generating, setGenerating] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false);
const [addOpen, setAddOpen] = useState(false);
const [zoom, setZoom] = useState(0.85);
const [pan, setPan] = useState({ x: 0, y: 0 });
const [size, setSize] = useState({ w: 1000, h: 640 });
const [hoverId, setHoverId] = useState(null);
const [dragging, setDragging] = useState(false);
const [hoverPortal, setHoverPortal] = useState(null);
const [waveText, setWaveText] = useState(null);
const [sprint, setSprint] = useState(false);
const [sprintLeft, setSprintLeft] = useState(60);
const [sparks, setSparks] = useState([]); // fuse spark fx
const [events, setEvents] = useState([
{ k: 'e1', who: 'Sam Lee', what: 'boosted "quirky"' },
{ k: 'e2', who: 'Alex Roy', what: 'shortlisted "Pawlio"' },
{ k: 'e3', who: 'Priya N', what: 'upvoted "Snugglo"' },
]);
const sceneRef = useRef(null);
const posRef = useRef(new Map());
const tgtRef = useRef(new Map());
const elsRef = useRef(new Map());
const linesRef = useRef(new Map());
const presEls = useRef(new Map());
const spotEls = useRef(new Map());
const dataRef = useRef({});
const panRef = useRef(pan); panRef.current = pan;
const zoomRef = useRef(zoom); zoomRef.current = zoom;
const dragRef = useRef(null); // pan drag
const cardDragRef = useRef(null); // card drag
const suppressClickRef = useRef(false);
const evK = useRef(4);
const visibleNames = useMemo(() => names.slice(0, density + Math.max(0, names.length - SEED_COUNT)), [names, density]);
// presence (simulated teammates)
const presence = useRef((window.BXpeople || []).slice(0, 3).map((p, i) => ({
id: 'pres-' + i, name: p.name, colour: ['#2DE2E6', '#FF3DA6', '#A8FF4D'][i % 3],
x: Math.cos((i / 3) * Math.PI * 2) * 230, y: Math.sin((i / 3) * Math.PI * 2) * 150 - 30,
ox: (Math.random() - 0.5) * 120, oy: 60 + Math.random() * 40, targetId: null,
}))).current;
const pushEvent = (who, what) => setEvents((ev) => [{ k: 'k' + (evK.current++), who, what }, ...ev].slice(0, 12));
// ---- layout: keyword wells around a ring; names by gravity ----------
const kwPos = useMemo(() => {
const m = new Map();
const Rkx = size.w / 2 - 90, Rky = size.h / 2 - 70;
keywords.forEach((k, i) => {
const a = (i / Math.max(1, keywords.length)) * Math.PI * 2 - Math.PI / 2;
m.set(k.id, { x: Math.cos(a) * Rkx, y: Math.sin(a) * Rky });
});
return m;
}, [keywords, size]);
useEffect(() => {
const Rx = size.w / 2 - 150, Ry = size.h / 2 - 120;
const GA = Math.PI * (3 - Math.sqrt(5));
const tgt = tgtRef.current;
const ids = new Set();
const order = [...visibleNames].sort((a, b) => hash(a.id) - hash(b.id));
const N = Math.max(1, order.length);
order.forEach((n, i) => {
ids.add(n.id);
const a = i * GA + (hash(n.id) % 24) / 24;
const norm = clamp((n.score + 16) / 42, 0, 1);
// score-driven radius: liked names sit near the centre, rejected drift to the edge
const rad = 0.30 + 0.62 * (1 - norm) + ((hash(n.id) % 5) / 5) * 0.06;
let tx = Math.cos(a) * Rx * rad, ty = Math.sin(a) * Ry * rad;
let ax = 0, ay = 0, aw = 0;
(n.keywords || []).forEach((kw) => {
const k = keywords.find((kk) => kk.label === kw);
if (k && (k.state === 'boosted' || k.state === 'favourite' || (k.weight || 1) > 1)) {
const p = kwPos.get(k.id); if (p) { ax += p.x * k.weight; ay += p.y * k.weight; aw += k.weight; }
}
});
if (aw > 0) { const b = clamp(aw * 0.12, 0, 0.5); tx = tx * (1 - b) + (ax / aw) * b; ty = ty * (1 - b) + (ay / aw) * b; }
tgt.set(n.id, { x: tx, y: ty });
// seed position + paint immediately so layout is correct even if rAF is throttled
if (!posRef.current.has(n.id)) {
posRef.current.set(n.id, { x: tx, y: ty });
const el = elsRef.current.get(n.id);
const norm0 = clamp((n.score + 16) / 42, 0, 1);
if (el) el.style.transform = `translate(-50%,-50%) translate3d(${tx}px, ${ty}px, ${norm0 * 55 - 8}px)`;
}
});
[...tgt.keys()].forEach((id) => { if (!ids.has(id)) tgt.delete(id); });
}, [visibleNames, keywords, kwPos, size]);
// measure scene
useEffect(() => {
const el = sceneRef.current; if (!el) return;
const measure = () => setSize({ w: el.clientWidth, h: el.clientHeight });
measure();
const ro = new ResizeObserver(measure); ro.observe(el);
return () => ro.disconnect();
}, [openId]);
// keep latest data for the rAF loop
const related = useMemo(() => relatedIds(names.find((n) => n.id === hoverId), visibleNames), [hoverId, visibleNames]);
dataRef.current = { names: visibleNames, motion, hoverId, related, openId, presence, dragId: cardDragRef.current && cardDragRef.current.id };
// ---- the physics / animation loop -----------------------------------
useEffect(() => {
let raf;
const tick = () => {
const now = performance.now();
const d = dataRef.current;
const pos = posRef.current, tgt = tgtRef.current;
window.__bx = { t: ((window.__bx && window.__bx.t) || 0) + 1, names: d.names ? d.names.length : -1, tgt: tgt.size, els: elsRef.current.size, sample: tgt.get('drabwell') };
d.names.forEach((n) => {
let p = pos.get(n.id);
const t = tgt.get(n.id) || { x: 0, y: 0 };
if (!p) { p = { x: (Math.random() - 0.5) * 40, y: (Math.random() - 0.5) * 40 }; pos.set(n.id, p); }
const drag = cardDragRef.current;
if (drag && drag.id === n.id && drag.pos) { p.x = drag.pos.x; p.y = drag.pos.y; }
else { p.x += (t.x - p.x) * 0.07; p.y += (t.y - p.y) * 0.07; }
const el = elsRef.current.get(n.id);
if (el) {
const phase = (hash(n.id) % 1000) / 1000 * Math.PI * 2;
const fl = d.motion ? Math.sin(now / 1100 + phase) * 4 : 0;
const norm = clamp((n.score + 16) / 42, 0, 1);
const z = norm * 55 - 8;
el.style.transform = `translate(-50%,-50%) translate3d(${p.x}px, ${(p.y + fl).toFixed(2)}px, ${z}px)`;
const dim = d.hoverId && d.hoverId !== n.id && !d.related.has(n.id);
el.style.opacity = dim ? 0.3 : 1;
el.style.zIndex = d.openId === n.id ? 50 : (n.heat === 'on_fire' ? 22 : 14);
}
});
// presence markers + spotlights
d.presence.forEach((u) => {
const tp = u.targetId && pos.get(u.targetId);
const tx = tp ? tp.x + u.ox : u.x, ty = tp ? tp.y + u.oy : u.y;
u.x += (tx - u.x) * 0.045; u.y += (ty - u.y) * 0.045;
const el = presEls.current.get(u.id);
if (el) el.style.transform = `translate(-50%,-50%) translate(${u.x.toFixed(1)}px, ${u.y.toFixed(1)}px)`;
const sp = spotEls.current.get(u.id);
if (sp) {
if (tp) { sp.style.opacity = 0.5; sp.style.transform = `translate(-50%,-50%) translate(${tp.x.toFixed(1)}px, ${tp.y.toFixed(1)}px)`; }
else sp.style.opacity = 0;
}
});
// constellation lines
if (d.hoverId) {
const hp = pos.get(d.hoverId);
linesRef.current.forEach((ln, id) => {
const mp = pos.get(id);
if (hp && mp) { ln.setAttribute('x1', hp.x.toFixed(1)); ln.setAttribute('y1', hp.y.toFixed(1)); ln.setAttribute('x2', mp.x.toFixed(1)); ln.setAttribute('y2', mp.y.toFixed(1)); }
});
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
// ---- presence behaviour: wander + vote ------------------------------
useEffect(() => {
const pick = setInterval(() => {
const ns = dataRef.current.names;
if (!ns.length) return;
presence.forEach((u) => { u.targetId = ns[Math.floor(Math.random() * ns.length)].id; u.ox = (Math.random() - 0.5) * 140; u.oy = 50 + Math.random() * 50; });
}, 2600);
return () => clearInterval(pick);
}, []);
useEffect(() => {
const ms = sprint ? 1200 : 4200;
const t = setInterval(() => {
const ns = dataRef.current.names;
if (!ns.length) return;
const u = presence[Math.floor(Math.random() * presence.length)];
const target = (u.targetId && ns.find((n) => n.id === u.targetId)) || ns[Math.floor(Math.random() * ns.length)];
const type = Math.random() < 0.72 ? 'up' : 'down';
applyVote(target.id, type, u.name);
}, ms);
return () => clearInterval(t);
}, [sprint]);
// sprint countdown
useEffect(() => {
if (!sprint) return;
setSprintLeft(60);
const t = setInterval(() => setSprintLeft((s) => { if (s <= 1) { clearInterval(t); setSprint(false); return 60; } return s - 1; }), 1000);
return () => clearInterval(t);
}, [sprint]);
// ---- interactions ---------------------------------------------------
useEffect(() => {
const el = sceneRef.current; if (!el) return;
const onWheel = (e) => { e.preventDefault(); setZoom((z) => clamp(z - e.deltaY * 0.0014, 0.3, 1.7)); };
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
useEffect(() => {
const onKey = (e) => {
const tag = document.activeElement && document.activeElement.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
const step = 60; let dx = 0, dy = 0;
if (e.key === 'ArrowLeft') dx = step; else if (e.key === 'ArrowRight') dx = -step;
else if (e.key === 'ArrowUp') dy = step; else if (e.key === 'ArrowDown') dy = -step;
else if (e.key === 'Escape') { setOpenId(null); return; } else return;
e.preventDefault(); setPan((p) => ({ x: p.x + dx, y: p.y + dy }));
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const toScene = (clientX, clientY) => {
const r = sceneRef.current.getBoundingClientRect();
return { x: (clientX - r.left - r.width / 2 - panRef.current.x) / zoomRef.current, y: (clientY - r.top - r.height / 2 - panRef.current.y) / zoomRef.current };
};
useEffect(() => {
const onMove = (e) => {
if (cardDragRef.current) {
const d = cardDragRef.current;
if (Math.abs(e.clientX - d.sx) > 4 || Math.abs(e.clientY - d.sy) > 4) d.moved = true;
d.pos = toScene(e.clientX, e.clientY);
// portal hover detection
const elBelow = document.elementFromPoint(e.clientX, e.clientY);
const portal = elBelow && elBelow.closest && elBelow.closest('[data-portal]');
setHoverPortal(portal ? portal.getAttribute('data-portal') : null);
return;
}
if (dragRef.current) setPan({ x: dragRef.current.px + (e.clientX - dragRef.current.x), y: dragRef.current.py + (e.clientY - dragRef.current.y) });
};
const onUp = (e) => {
if (cardDragRef.current) {
const d = cardDragRef.current;
if (d.moved) {
suppressClickRef.current = true;
const below = document.elementFromPoint(e.clientX, e.clientY);
const portal = below && below.closest && below.closest('[data-portal]');
const card = below && below.closest && below.closest('[data-name-id]');
if (portal) evolveName(d.id, portal.getAttribute('data-portal'));
else if (card && card.getAttribute('data-name-id') !== d.id) fuseNames(d.id, card.getAttribute('data-name-id'), e.clientX, e.clientY);
}
cardDragRef.current = null; setDragging(false); setHoverPortal(null); document.body.style.cursor = '';
}
if (dragRef.current) { dragRef.current = null; document.body.style.cursor = ''; }
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
});
const onCardDown = (e, n) => {
if (e.button !== 0 || (e.target.closest && e.target.closest('button'))) return;
e.stopPropagation();
const p = posRef.current.get(n.id) || { x: 0, y: 0 };
cardDragRef.current = { id: n.id, sx: e.clientX, sy: e.clientY, moved: false, pos: { ...p } };
setDragging(true); document.body.style.cursor = 'grabbing';
};
const onSceneDown = (e) => {
const onCard = e.target.closest && e.target.closest('.bx-nc-mount');
if (e.button === 1 || (e.button === 0 && !onCard)) { dragRef.current = { x: e.clientX, y: e.clientY, px: pan.x, py: pan.y }; document.body.style.cursor = 'grabbing'; e.preventDefault(); }
};
// ---- mutations ------------------------------------------------------
function applyVote(id, type, who) {
setNames((ns) => ns.map((n) => {
if (n.id !== id) return n;
if (type === 'star') { const sl = !n.shortlisted; const sc = scoreOf(n.counts, sl); return { ...n, shortlisted: sl, score: sc, heat: heatFromVotes(n.counts.up || 0, n.counts.down || 0) }; }
if (type !== 'up' && type !== 'down') return n;
const counts = { ...n.counts, [type]: (n.counts[type] || 0) + 1 };
return { ...n, counts, score: scoreOf(counts, n.shortlisted), heat: heatFromVotes(counts.up || 0, counts.down || 0) };
}));
const nm = dataRef.current.names.find((n) => n.id === id);
const lbl = { up: 'upvoted', down: 'downvoted', star: 'shortlisted' }[type];
pushEvent(who || 'You', `${lbl} "${nm ? nm.name : ''}"`);
}
const vote = (id, type) => applyVote(id, type, 'You');
const adjustKw = (id, dir) => {
setKeywords((ks) => ks.map((k) => {
if (k.id !== id) return k;
if (dir > 0) return { ...k, weight: (k.weight || 1) + 1, count: (k.count || 0) + 1, state: k.state === 'avoid' ? 'avoid' : 'boosted' };
const weight = Math.max(0, (k.weight || 1) - 1);
return { ...k, weight, count: Math.max(0, (k.count || 0) - 1) || null, state: weight <= 1 ? 'cooling' : k.state };
}));
const kw = keywords.find((k) => k.id === id);
pushEvent('You', `${dir > 0 ? 'boosted' : 'cooled'} "${kw ? kw.label : ''}"`);
};
const deleteKw = (id) => { const kw = keywords.find((k) => k.id === id); setKeywords((ks) => ks.filter((k) => k.id !== id)); pushEvent('You', `removed keyword "${kw ? kw.label : ''}"`); };
const moreKeywords = () => {
setKeywords((ks) => { const have = new Set(ks.map((k) => k.id)); const pick = KW_POOL.filter((w) => !have.has(w)).slice(0, 2); return pick.length ? [...ks, ...pick.map((w) => ({ id: w, label: w, state: 'normal', weight: 1, count: null }))] : ks; });
pushEvent('Brainstorm X', 'suggested new keywords');
};
const spawn = (cards) => setNames((ns) => { cards.forEach((c) => posRef.current.delete(c.id)); return [...ns, ...cards]; });
const moreNames = () => {
const have = new Set(names.map((n) => n.id));
const pick = NAME_POOL.find((p) => !have.has(p.id)); if (!pick) return;
const up = 3 + Math.floor(Math.random() * 3);
spawn([{ ...pick, confidence: 62 + Math.floor(Math.random() * 24), counts: { up, down: 1 }, score: up * 2 - 2, heat: heatFromVotes(up, 1), wave }]);
pushEvent('Brainstorm X', `generated "${pick.name}" from your votes`);
};
const deleteName = (id) => { const nm = names.find((n) => n.id === id); if (openId === id) setOpenId(null); posRef.current.delete(id); setNames((ns) => ns.filter((n) => n.id !== id)); pushEvent('You', `removed "${nm ? nm.name : ''}"`); };
const addCustomName = (name, styl) => {
const id = 'custom-' + name.toLowerCase().replace(/[^a-z0-9]/g, '') + '-' + Date.now().toString(36);
spawn([{ id, name, style: styl || 'brandable', pronunciation: '', reason: 'Added by you β vote it up or down with the team.', keywords: [], confidence: null, counts: { up: 0, down: 0 }, score: 0, heat: 'neutral', custom: true, wave }]);
pushEvent('You', `added custom name "${name}"`);
};
const evolveName = (id, dir) => {
const parent = names.find((n) => n.id === id); if (!parent) return;
const cfg = EVOLVE[dir] || EVOLVE.like;
const base = parent.name.replace(/[^A-Za-z]/g, '');
const kids = cfg.mk(base).slice(0, 2).map((nm, i) => {
const up = 2 + Math.floor(Math.random() * 3);
return { id: `evo-${id}-${dir}-${i}-${Date.now().toString(36)}`, name: nm.charAt(0).toUpperCase() + nm.slice(1), style: parent.style, pronunciation: '', reason: `Evolved from β${parent.name}β β ${cfg.lbl}.`, keywords: parent.keywords, confidence: 60 + Math.floor(Math.random() * 22), counts: { up, down: 0 }, score: up * 2, heat: heatFromVotes(up, 0), parentNameIds: [id], wave };
});
spawn(kids);
setWaveText(`Evolved β${parent.name}β β ${cfg.lbl} Β· ${kids.map((k) => k.name).join(', ')}`);
pushEvent('Brainstorm X', `evolved "${parent.name}" β ${cfg.lbl}`);
clearWaveText();
};
const fuseNames = (aId, bId, cx, cy) => {
const a = names.find((n) => n.id === aId), b = names.find((n) => n.id === bId); if (!a || !b) return;
const A = a.name.replace(/[^A-Za-z]/g, ''), B = b.name.replace(/[^A-Za-z]/g, '');
const blends = [A.slice(0, Math.ceil(A.length / 2)) + B.slice(Math.floor(B.length / 2)), B.slice(0, Math.ceil(B.length / 2)) + A.slice(Math.floor(A.length / 2)), A.slice(0, 3) + B.slice(0, 3)];
const kids = [...new Set(blends)].slice(0, 3).map((nm, i) => {
const up = 2 + Math.floor(Math.random() * 3);
return { id: `fuse-${i}-${Date.now().toString(36)}`, name: nm.charAt(0).toUpperCase() + nm.slice(1).toLowerCase(), style: 'compound', pronunciation: '', reason: `Fused from β${a.name}β + β${b.name}β.`, keywords: [...new Set([...(a.keywords || []), ...(b.keywords || [])])].slice(0, 3), confidence: 58 + Math.floor(Math.random() * 24), counts: { up, down: 0 }, score: up * 2, heat: heatFromVotes(up, 0), parentNameIds: [aId, bId], wave };
});
spawn(kids);
const sk = toScene(cx, cy);
const sid = 's' + Date.now();
setSparks((s) => [...s, { id: sid, x: sk.x, y: sk.y }]);
setTimeout(() => setSparks((s) => s.filter((x) => x.id !== sid)), 700);
setWaveText(`Fused β${a.name}β + β${b.name}β β ${kids.map((k) => k.name).join(', ')}`);
pushEvent('Brainstorm X', `fused "${a.name}" + "${b.name}"`);
clearWaveText();
};
const waveTimer = useRef(null);
const clearWaveText = () => { clearTimeout(waveTimer.current); waveTimer.current = setTimeout(() => setWaveText(null), 6000); };
const generate = () => {
setGenerating(true);
const boosted = keywords.filter((k) => k.state === 'boosted' || k.state === 'favourite' || (k.weight || 1) > 1).map((k) => k.label);
const from = (boosted.length ? boosted : ['playful', 'warm', 'short']).slice(0, 4).join(', ');
setTimeout(() => {
const batch = [
{ id: 'pip-' + wave, name: 'Pippa & Co', style: 'short_phrase', pronunciation: 'pip-uh', reason: 'Playful, personable and clearly British in tone.', keywords: ['quirky', 'warm'], confidence: 77, score: 10, counts: { up: 5, down: 0 }, heat: heatFromVotes(5, 0), wave: wave + 1 },
{ id: 'wag-' + wave, name: 'Waggle', style: 'real_word', pronunciation: 'wag-uhl', reason: 'Fun, energetic and easy to remember for a pet brand.', keywords: ['playful', 'fun'], confidence: 73, score: 6, counts: { up: 4, down: 1 }, heat: heatFromVotes(4, 1), wave: wave + 1 },
{ id: 'mlo-' + wave, name: 'Marlo Pets', style: 'compound', pronunciation: 'mar-loh', reason: 'Warmer, more premium feel from the boosted keywords.', keywords: ['premium', 'warm'], confidence: 70, score: 4, counts: { up: 3, down: 1 }, heat: heatFromVotes(3, 1), wave: wave + 1 },
];
spawn(batch);
setWave((w) => w + 1);
setWaveText(`Wave ${wave + 1} Β· generated from: ${from}`);
pushEvent('Brainstorm X', `generated Wave ${wave + 1}`);
clearWaveText();
setGenerating(false);
}, 1200);
};
const open = names.find((n) => n.id === openId);
const sceneRight = open ? 360 : 40;
const cx = size.w / 2, cy = size.h / 2;
return (
setInviteOpen(true)} onMoreNames={moreNames} onAddCustom={() => setAddOpen(true)}
sprint={sprint} onToggleSprint={() => setSprint((s) => !s)} />
{ setZoom(0.85); setPan({ x: 0, y: 0 }); }} />
{sprint && }
{/* scene */}
{/* constellation lines */}
{hoverId && (
)}
{/* keyword gravity wells */}
{keywords.map((k) => {
const p = kwPos.get(k.id) || { x: 0, y: 0 };
return (
adjustKw(k.id, +1)} onCool={() => adjustKw(k.id, -1)} onDelete={() => deleteKw(k.id)} />
);
})}
{/* presence spotlights (behind cards) */}
{presence.map((u) => (
{ if (el) spotEls.current.set(u.id, el); }} style={{ position: 'absolute', left: '50%', top: '50%', width: 260, height: 180, borderRadius: '50%', pointerEvents: 'none', opacity: 0, zIndex: 7, background: `radial-gradient(circle, color-mix(in srgb, ${u.colour} 22%, transparent), transparent 70%)`, transition: 'opacity var(--dur-base)' }} />
))}
{/* name cards */}
{visibleNames.map((n) => (
{ if (el) { elsRef.current.set(n.id, el); const p = posRef.current.get(n.id) || tgtRef.current.get(n.id); el.style.transform = `translate(-50%,-50%) translate3d(${p ? p.x : 0}px, ${p ? p.y : 0}px, 0)`; } else elsRef.current.delete(n.id); }}
onMouseEnter={() => setHoverId(n.id)} onMouseLeave={() => setHoverId((h) => (h === n.id ? null : h))}
style={{ position: 'absolute', left: '50%', top: '50%', willChange: 'transform' }}>
vote(n.id, t)} onCardDown={(e) => onCardDown(e, n)}
onOpen={() => { if (suppressClickRef.current) { suppressClickRef.current = false; return; } setOpenId(n.id); }}
onDelete={() => deleteName(n.id)} />
))}
{/* presence markers */}
{presence.map((u) => (
{ if (el) { presEls.current.set(u.id, el); el.style.transform = `translate(-50%,-50%) translate(${u.x}px, ${u.y}px)`; } }} style={{ position: 'absolute', left: '50%', top: '50%', zIndex: 30, pointerEvents: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
{u.name.split(' ')[0]}
))}
{/* fuse sparks */}
{sparks.map((s) => (
))}
{/* generation portal flash */}
{generating && (
)}
{open &&
setOpenId(null)} onEvolve={evolveName} onOpen={(id) => setOpenId(id)} onVote={vote} />}
{inviteOpen && setInviteOpen(false)} />}
{addOpen && setAddOpen(false)} />}
);
}
window.BXArena = Arena;
})();