/* hero-bubbles.jsx — animated hero visualization, 3 variants */ const HeroBubbles = ({ variant = 'cloud', skin }) => { const [tick, setTick] = React.useState(0); React.useEffect(() => { let f; const loop = () => { setTick((t) => t + 1); f = requestAnimationFrame(loop); }; f = requestAnimationFrame(loop); return () => cancelAnimationFrame(f); }, []); // Master label set — 5 large (text shown), rest medium/small (no text) const labels = [ { label: 'Premiumization & Sophistication', kind: 'teal', r: 13 }, // 0 L { label: 'Alcohol-Free Efficiency', kind: 'warm', r: 12 }, // 1 L { label: 'Mindful Drinking', kind: 'teal', r: 11 }, // 2 L { label: 'Functional Hydration', kind: 'warm', r: 10 }, // 3 L { label: 'Sensory Premium Cues', kind: 'teal', r: 10 }, // 4 L { label: 'Price-Value Friction', kind: 'warm', r: 6 }, // 5 M { label: 'Ritual & Routine', kind: 'teal', r: 6 }, // 6 M { label: 'Sober Curious', kind: 'warm', r: 6 }, // 7 M { label: 'Flavor as Identity', kind: 'teal', r: 5 }, // 8 M { label: 'THC as Substitute', kind: 'dim', r: 3 }, // 9 small dim { label: 'Occasion Substitution', kind: 'teal', r: 5 }, // 10 M { label: 'Glorified Juice', kind: 'warm', r: 4 }, // 11 S { label: 'Sleepy Girl Mocktail', kind: 'teal', r: 4 }, // 12 S { label: 'Adaptogens & Botanicals', kind: 'dim', r: 3 }, // 13 small dim { label: 'DIY Flavor', kind: 'dim', r: 2.5 }, // 14 small dim { label: 'Cost-of-Living Trade-offs', kind: 'dim', r: 2.5 }, // 15 small dim { label: 'Caffeine Crossover', kind: 'dim', r: 2 }, // 16 small dim { label: 'Pregnancy & Postpartum', kind: 'dim', r: 2 } // 17 small dim ]; // Cloud layout — large nodes occupy core, small nodes fill periphery const cloud = [ { x: 36, y: 44 }, // 0 L { x: 66, y: 36 }, // 1 L { x: 56, y: 66 }, // 2 L { x: 24, y: 66 }, // 3 L { x: 80, y: 62 }, // 4 L { x: 50, y: 22 }, // 5 M { x: 14, y: 38 }, // 6 M { x: 84, y: 22 }, // 7 M { x: 42, y: 84 }, // 8 M { x: 72, y: 84 }, // 9 M { x: 14, y: 84 }, // 10 M { x: 26, y: 16 }, // 11 S { x: 66, y: 14 }, // 12 S { x: 92, y: 42 }, // 13 S { x: 6, y: 56 }, // 14 S { x: 90, y: 84 }, // 15 S { x: 8, y: 20 }, // 16 S { x: 56, y: 50 } // 17 S — fills mid-right ]; const layouts = { cloud, orbit: (() => { const arr = [{ x: 50, y: 50 }]; const ringCount = labels.length - 1; for (let i = 0; i < ringCount; i++) { const a = i / ringCount * Math.PI * 2 - Math.PI / 2; const radius = i % 3 === 0 ? 38 : i % 3 === 1 ? 28 : 20; arr.push({ x: 50 + Math.cos(a) * radius, y: 50 + Math.sin(a) * radius }); } return arr; })(), grid: (() => { const arr = []; const cols = 6,rows = 3; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { arr.push({ x: 10 + c * 16, y: 22 + r * 28 }); } } return arr; })() }; const positions = layouts[variant] || layouts.cloud; const bubbles = labels.map((l, i) => ({ ...l, ...(positions[i] || { x: 50, y: 50 }) })); const floatOffset = (i, t) => { if (variant === 'grid') { const a = t / 80 + i * 1.3; return { dx: Math.sin(a) * 0.4, dy: Math.cos(a) * 0.4 }; } if (variant === 'orbit') { if (i === 0) return { dx: 0, dy: 0 }; const ringCount = labels.length - 1; const speed = i % 2 === 0 ? 0.0022 : -0.0016; const baseA = (i - 1) / ringCount * Math.PI * 2 - Math.PI / 2; const a = baseA + t * speed; const r = (i - 1) % 3 === 0 ? 38 : (i - 1) % 3 === 1 ? 28 : 20; const tx = 50 + Math.cos(a) * r; const ty = 50 + Math.sin(a) * r; return { dx: tx - positions[i].x, dy: ty - positions[i].y }; } const a = t / 60 + i * 1.7; return { dx: Math.sin(a) * 0.9, dy: Math.cos(a * 0.9) * 0.7 }; }; // Connection lines — between meaningful pairs const linesByVariant = { cloud: [ [0, 1, 'warm'], [0, 2, 'teal'], [1, 4, 'warm'], [2, 3, 'teal'], [0, 3, 'warm'], [1, 5, 'warm'], [2, 8, 'teal'], [3, 10, 'teal'], [4, 9, 'warm'], [0, 6, 'teal'], [1, 7, 'warm'], [5, 11, 'warm'], [5, 12, 'teal'], [2, 17, 'teal']], orbit: Array.from({ length: labels.length - 1 }, (_, i) => [0, i + 1, i % 2 ? 'warm' : 'teal']), grid: [[0, 7, 'teal'], [2, 8, 'warm'], [3, 9, 'teal'], [4, 10, 'warm'], [6, 12, 'teal'], [7, 13, 'warm'], [8, 14, 'teal'], [9, 15, 'warm'], [10, 16, 'teal']] }; const lines = linesByVariant[variant] || linesByVariant.cloud; // Curved-path builder — quadratic Bezier with a perpendicular control point const curvePath = (ax, ay, bx, by, curveIdx) => { const mx = (ax + bx) / 2; const my = (ay + by) / 2; const dx = bx - ax; const dy = by - ay; const len = Math.sqrt(dx * dx + dy * dy) || 1; // Perpendicular unit vector const px = -dy / len; const py = dx / len; // Alternate curve direction so bundles don't overlap const dir = curveIdx % 2 === 0 ? 1 : -1; const amount = Math.min(8, len * 0.22) * dir; const cx = mx + px * amount; const cy = my + py * amount; return `M ${ax} ${ay} Q ${cx} ${cy} ${bx} ${by}`; }; return (