/* React Bits ParticleText — vendored JS-CSS, adapted for Babel standalone.
   Source: https://github.com/DavidHDev/react-bits ParticleText
   Homepage also samples the Helix mark with the same particle engine. */
(function () {
  const { useEffect, useRef } = React;

  const hexToRgb = (hex) => {
    const clean = String(hex || '').replace('#', '').trim();
    if (!/^[0-9a-fA-F]{6}$/.test(clean)) return null;
    return {
      r: parseInt(clean.slice(0, 2), 16),
      g: parseInt(clean.slice(2, 4), 16),
      b: parseInt(clean.slice(4, 6), 16),
    };
  };
  const mixRgb = (from, to, amount) => ({
    r: Math.round(from.r + (to.r - from.r) * amount),
    g: Math.round(from.g + (to.g - from.g) * amount),
    b: Math.round(from.b + (to.b - from.b) * amount),
  });
  const rgbToCss = (rgb) => `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
  const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);

  const resolveFontSize = (value, container, fontWeight, fontFamily) => {
    if (typeof value === 'number') return value;
    const probe = document.createElement('span');
    probe.textContent = 'M';
    probe.style.position = 'absolute';
    probe.style.visibility = 'hidden';
    probe.style.pointerEvents = 'none';
    probe.style.fontSize = value;
    probe.style.fontWeight = String(fontWeight);
    probe.style.fontFamily = fontFamily;
    container.appendChild(probe);
    const size = parseFloat(window.getComputedStyle(probe).fontSize) || 96;
    probe.remove();
    return size;
  };

  const waitForFonts = async (font) => {
    if (!('fonts' in document)) return;
    try { await document.fonts.load(font); } catch (_) {}
    await document.fonts.ready;
  };

  function drawHelixMark(ctx, originX, originY, size) {
    const s = size / 128;
    const r = (x, y, w, h, fill) => {
      ctx.fillStyle = fill;
      ctx.fillRect(originX + x * s, originY + y * s, w * s, h * s);
    };
    r(0, 0, 16, 128, '#020202');
    r(112, 0, 16, 128, '#020202');
    r(16, 76, 24, 24, '#E8B33B');
    r(40, 60, 24, 24, '#E8B33B');
    r(64, 44, 24, 24, '#E8B33B');
    r(88, 28, 24, 24, '#E8B33B');
  }

  function useParticleEngine({
    sample,
    particleSize,
    density,
    color,
    highlightColor,
    scatter,
    gatherDuration,
    stagger,
    pointerRepel,
    repelRadius,
    idleDrift,
    trigger,
    glow,
  }) {
    const containerRef = useRef(null);
    const canvasRef = useRef(null);

    useEffect(() => {
      if (typeof window === 'undefined') return undefined;
      const container = containerRef.current;
      const canvas = canvasRef.current;
      if (!container || !canvas) return undefined;
      const ctx = canvas.getContext('2d');
      if (!ctx) return undefined;

      let particles = [];
      let animationFrame = null;
      let resizeFrame = null;
      let buildId = 0;
      let gathering = false;
      let gatherStart = 0;
      let reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
      let width = 0;
      let height = 0;
      const pointer = { active: false, x: 0, y: 0, smoothX: 0, smoothY: 0 };

      const startGather = (fromScatter = true) => {
        if (!particles.length) return;
        const now = performance.now();
        const spread = reducedMotion ? 0 : scatter;
        particles.forEach((particle) => {
          if (fromScatter) {
            const angle = particle.seed * Math.PI * 2;
            const distance = spread * (0.35 + particle.depth * 0.75);
            particle.x = particle.targetX + Math.cos(angle) * distance + (particle.depth - 0.5) * spread * 0.55;
            particle.y = particle.targetY + Math.sin(angle) * distance + (particle.seed - 0.5) * spread * 0.55;
          }
          particle.startX = particle.x;
          particle.startY = particle.y;
          particle.delay = reducedMotion ? 0 : particle.seed * stagger;
        });
        gatherStart = now;
        gathering = true;
      };

      const drawParticle = (particle) => {
        const size = particle.size;
        ctx.fillStyle = particle.color;
        if (size <= 2.1) {
          ctx.fillRect(particle.x - size / 2, particle.y - size / 2, size, size);
          return;
        }
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, size / 2, 0, Math.PI * 2);
        ctx.fill();
      };

      const render = (now) => {
        ctx.clearRect(0, 0, width, height);
        if (glow && !reducedMotion) {
          ctx.shadowBlur = particleSize * 3;
          ctx.shadowColor = highlightColor;
        } else {
          ctx.shadowBlur = 0;
        }
        pointer.smoothX += (pointer.x - pointer.smoothX) * 0.18;
        pointer.smoothY += (pointer.y - pointer.smoothY) * 0.18;
        let complete = true;
        particles.forEach((particle) => {
          let baseX = particle.targetX;
          let baseY = particle.targetY;
          let progress = 1;
          if (gathering) {
            const local = (now - gatherStart - particle.delay) / Math.max(1, reducedMotion ? 1 : gatherDuration);
            progress = clamp(local, 0, 1);
            const eased = easeOutCubic(progress);
            baseX = particle.startX + (particle.targetX - particle.startX) * eased;
            baseY = particle.startY + (particle.targetY - particle.startY) * eased;
            if (progress < 1) complete = false;
          } else if (!reducedMotion && idleDrift > 0) {
            const driftTime = now * 0.001;
            baseX += Math.sin(driftTime * 0.9 + particle.seed * 10) * idleDrift * particle.depth;
            baseY += Math.cos(driftTime * 0.75 + particle.depth * 10) * idleDrift * particle.depth;
          }
          if (pointer.active && !reducedMotion && pointerRepel > 0 && repelRadius > 0) {
            const dx = baseX - pointer.smoothX;
            const dy = baseY - pointer.smoothY;
            const distance = Math.hypot(dx, dy);
            if (distance > 0 && distance < repelRadius) {
              const force = Math.pow(1 - distance / repelRadius, 2) * pointerRepel;
              baseX += (dx / distance) * force;
              baseY += (dy / distance) * force;
            }
          }
          const follow = reducedMotion ? 1 : 0.22;
          particle.x += (baseX - particle.x) * follow;
          particle.y += (baseY - particle.y) * follow;
          ctx.globalAlpha = clamp(0.35 + progress * 0.65, 0, 1);
          drawParticle(particle);
        });
        ctx.globalAlpha = 1;
        ctx.shadowBlur = 0;
        if (gathering && complete) gathering = false;
        animationFrame = window.requestAnimationFrame(render);
      };

      const ensureRenderLoop = () => {
        if (animationFrame === null) animationFrame = window.requestAnimationFrame(render);
      };

      const buildFromTargets = (targets) => {
        const maxParticles = Math.max(900, Math.min(6200, Math.floor((width * height) / 90)));
        const stride = Math.max(1, Math.ceil(targets.length / maxParticles));
        const baseRgb = hexToRgb(color);
        const highlightRgb = hexToRgb(highlightColor);
        const selected = targets.filter((_, index) => index % stride === 0);
        particles = selected.map((target, index) => {
          const seed = ((index * 9301 + 49297) % 233280) / 233280;
          const depth = 0.45 + (((index * 233 + 97) % 1000) / 1000) * 0.9;
          const blend = baseRgb && highlightRgb ? clamp(target.x / Math.max(1, width) + (seed - 0.5) * 0.35, 0, 1) : 0;
          const particleColor = target.color
            || (baseRgb && highlightRgb ? rgbToCss(mixRgb(baseRgb, highlightRgb, blend)) : color);
          const angle = seed * Math.PI * 2;
          const distance = (reducedMotion ? 0 : scatter) * (0.35 + depth * 0.75);
          const startX = target.x + Math.cos(angle) * distance + (seed - 0.5) * scatter * 0.45;
          const startY = target.y + Math.sin(angle) * distance + (depth - 0.9) * scatter * 0.45;
          return {
            x: reducedMotion ? target.x : startX,
            y: reducedMotion ? target.y : startY,
            startX,
            startY,
            targetX: target.x,
            targetY: target.y,
            size: Math.max(0.6, particleSize * (0.75 + target.alpha * 0.45)),
            color: particleColor,
            seed,
            depth,
            delay: seed * stagger,
          };
        });
        pointer.x = width / 2;
        pointer.y = height / 2;
        pointer.smoothX = pointer.x;
        pointer.smoothY = pointer.y;
        if (reducedMotion) {
          particles.forEach((particle) => {
            particle.x = particle.targetX;
            particle.y = particle.targetY;
            particle.startX = particle.targetX;
            particle.startY = particle.targetY;
            particle.delay = 0;
          });
          gathering = false;
        } else {
          startGather(false);
        }
        ensureRenderLoop();
      };

      let lastW = 0;
      let lastH = 0;
      const rebuild = async (force) => {
        const rect = container.getBoundingClientRect();
        const nextW = Math.floor(rect.width);
        const nextH = Math.floor(rect.height);
        if (nextW <= 0 || nextH <= 0) return;
        if (!force && nextW === lastW && nextH === lastH) return;
        lastW = nextW;
        lastH = nextH;
        const currentBuild = ++buildId;
        width = nextW;
        height = nextH;
        const dpr = Math.min(window.devicePixelRatio || 1, 2);
        canvas.width = Math.max(1, Math.floor(width * dpr));
        canvas.height = Math.max(1, Math.floor(height * dpr));
        canvas.style.width = '100%';
        canvas.style.height = '100%';
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        const targets = await sample({
          width,
          height,
          container,
          density: Math.max(2, Math.floor(density)),
        });
        if (currentBuild !== buildId) return;
        buildFromTargets(targets || []);
      };

      const queueSample = () => {
        if (resizeFrame) window.cancelAnimationFrame(resizeFrame);
        resizeFrame = window.requestAnimationFrame(() => { rebuild(false); });
      };

      const handlePointerMove = (event) => {
        const rect = canvas.getBoundingClientRect();
        pointer.x = event.clientX - rect.left;
        pointer.y = event.clientY - rect.top;
        pointer.active = true;
      };
      const handlePointerLeave = () => { pointer.active = false; };
      const handlePointerEnter = (event) => {
        handlePointerMove(event);
        if (trigger === 'hover') startGather(true);
      };
      const handleClick = () => { if (trigger === 'click') startGather(true); };
      const reduceMotionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');
      const handleReduceMotionChange = (event) => {
        reducedMotion = event.matches;
        rebuild(true);
      };

      reduceMotionQuery?.addEventListener('change', handleReduceMotionChange);
      canvas.addEventListener('pointerenter', handlePointerEnter);
      canvas.addEventListener('pointermove', handlePointerMove);
      canvas.addEventListener('pointerleave', handlePointerLeave);
      canvas.addEventListener('click', handleClick);
      const resizeObserver = new ResizeObserver(queueSample);
      resizeObserver.observe(container);
      rebuild();

      return () => {
        buildId += 1;
        resizeObserver.disconnect();
        reduceMotionQuery?.removeEventListener('change', handleReduceMotionChange);
        canvas.removeEventListener('pointerenter', handlePointerEnter);
        canvas.removeEventListener('pointermove', handlePointerMove);
        canvas.removeEventListener('pointerleave', handlePointerLeave);
        canvas.removeEventListener('click', handleClick);
        if (animationFrame !== null) window.cancelAnimationFrame(animationFrame);
        if (resizeFrame !== null) window.cancelAnimationFrame(resizeFrame);
      };
    }, [
      sample, particleSize, density, color, highlightColor, scatter, gatherDuration,
      stagger, pointerRepel, repelRadius, idleDrift, trigger, glow,
    ]);

    return { containerRef, canvasRef };
  }

  function ParticleStage({ label, sample, className, style, ...physics }) {
    const { containerRef, canvasRef } = useParticleEngine({ sample, ...physics });
    return (
      <div ref={containerRef} className={'particle-text ' + (className || '')} style={style} aria-label={label}>
        <canvas ref={canvasRef} className="particle-text__canvas" aria-hidden="true" />
        <span className="particle-text__sr">{label}</span>
      </div>
    );
  }

  function ParticleText({
    text = 'HELIX',
    particleSize = 2,
    density = 8,
    color = '#020202',
    highlightColor = '#E8B33B',
    scatter = 180,
    gatherDuration = 1600,
    stagger = 780,
    pointerRepel = 72,
    repelRadius = 120,
    idleDrift = 1.6,
    trigger = 'mount',
    fontSize = 'clamp(3rem, 12vw, 8rem)',
    fontWeight = 600,
    fontFamily = 'Geist Mono, ui-monospace, monospace',
    glow = false,
    className = '',
    style,
  }) {
    const sample = React.useCallback(async ({ width, height, container, density: step }) => {
      const computed = window.getComputedStyle(container);
      const resolvedFamily = fontFamily === 'inherit' ? computed.fontFamily || 'sans-serif' : fontFamily;
      let resolvedSize = resolveFontSize(fontSize, container, fontWeight, resolvedFamily);
      let font = `${fontWeight} ${resolvedSize}px ${resolvedFamily}`;
      await waitForFonts(font);
      const offscreen = document.createElement('canvas');
      const offCtx = offscreen.getContext('2d', { willReadFrequently: true });
      if (!offCtx) return [];
      const content = String(text || ' ');
      const maxTextWidth = width * 0.92;
      offCtx.font = font;
      let metrics = offCtx.measureText(content);
      const measuredWidth = Math.max(1, metrics.width);
      if (measuredWidth > maxTextWidth) {
        resolvedSize = Math.max(18, resolvedSize * (maxTextWidth / measuredWidth));
        font = `${fontWeight} ${resolvedSize}px ${resolvedFamily}`;
        await waitForFonts(font);
        offCtx.font = font;
        metrics = offCtx.measureText(content);
      }
      const left = Math.ceil(metrics.actualBoundingBoxLeft || 0);
      const right = Math.ceil(metrics.actualBoundingBoxRight || metrics.width);
      const ascent = Math.ceil(metrics.actualBoundingBoxAscent || resolvedSize * 0.78);
      const descent = Math.ceil(metrics.actualBoundingBoxDescent || resolvedSize * 0.22);
      const padding = Math.max(12, Math.ceil(resolvedSize * 0.08));
      const textWidth = Math.max(1, left + right);
      const textHeight = Math.max(1, ascent + descent);
      offscreen.width = textWidth + padding * 2;
      offscreen.height = textHeight + padding * 2;
      offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
      offCtx.font = font;
      offCtx.textAlign = 'left';
      offCtx.textBaseline = 'alphabetic';
      offCtx.fillStyle = '#ffffff';
      offCtx.fillText(content, padding - left, padding + ascent);
      const imageData = offCtx.getImageData(0, 0, offscreen.width, offscreen.height);
      const targets = [];
      for (let y = 0; y < offscreen.height; y += step) {
        for (let x = 0; x < offscreen.width; x += step) {
          const alpha = imageData.data[(y * offscreen.width + x) * 4 + 3];
          if (alpha > 40) {
            targets.push({
              x: width / 2 - offscreen.width / 2 + x,
              y: height / 2 - offscreen.height / 2 + y,
              alpha: alpha / 255,
            });
          }
        }
      }
      return targets;
    }, [text, fontSize, fontWeight, fontFamily]);

    return (
      <ParticleStage
        label={text}
        sample={sample}
        className={className}
        style={style}
        particleSize={particleSize}
        density={density}
        color={color}
        highlightColor={highlightColor}
        scatter={scatter}
        gatherDuration={gatherDuration}
        stagger={stagger}
        pointerRepel={pointerRepel}
        repelRadius={repelRadius}
        idleDrift={idleDrift}
        trigger={trigger}
        glow={glow}
      />
    );
  }

  function ParticleHelix({
    particleSize = 2.6,
    density = 8,
    scatter = 220,
    gatherDuration = 1600,
    stagger = 780,
    pointerRepel = 72,
    repelRadius = 140,
    idleDrift = 1.6,
    trigger = 'mount',
    glow = false,
    className = '',
    style,
  }) {
    const sample = React.useCallback(async ({ width, height, density: step }) => {
      const mark = Math.min(width, height) * 0.62;
      const originX = (width - mark) / 2;
      const originY = (height - mark) / 2;
      const offscreen = document.createElement('canvas');
      offscreen.width = Math.max(1, Math.ceil(width));
      offscreen.height = Math.max(1, Math.ceil(height));
      const offCtx = offscreen.getContext('2d', { willReadFrequently: true });
      if (!offCtx) return [];
      drawHelixMark(offCtx, originX, originY, mark);
      const imageData = offCtx.getImageData(0, 0, offscreen.width, offscreen.height);
      const targets = [];
      for (let y = 0; y < offscreen.height; y += step) {
        for (let x = 0; x < offscreen.width; x += step) {
          const i = (y * offscreen.width + x) * 4;
          const alpha = imageData.data[i + 3];
          if (alpha > 40) {
            targets.push({
              x,
              y,
              alpha: alpha / 255,
              color: rgbToCss({
                r: imageData.data[i],
                g: imageData.data[i + 1],
                b: imageData.data[i + 2],
              }),
            });
          }
        }
      }
      return targets;
    }, []);

    return (
      <ParticleStage
        label="HelixCyber mark"
        sample={sample}
        className={className}
        style={style}
        particleSize={particleSize}
        density={density}
        color="#020202"
        highlightColor="#E8B33B"
        scatter={scatter}
        gatherDuration={gatherDuration}
        stagger={stagger}
        pointerRepel={pointerRepel}
        repelRadius={repelRadius}
        idleDrift={idleDrift}
        trigger={trigger}
        glow={glow}
      />
    );
  }

  window.ParticleText = ParticleText;
  window.ParticleHelix = ParticleHelix;
})();
