/* 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 && ( {[...related].map((id) => ( { if (el) linesRef.current.set(id, el); else linesRef.current.delete(id); }} stroke="url(#bxLine)" strokeWidth="1.5" strokeLinecap="round" style={{ filter: 'drop-shadow(0 0 4px rgba(45,226,230,0.6))' }} /> ))} )} {/* 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; })();