/* TEN GROW website UI kit — shared layout helpers.
   Exports to window so each screen file can use them. */

function Container({ children, size = "wide", style, className }) {
  const widths = { wide: "var(--container-wide)", base: "var(--container)", narrow: "var(--container-narrow)", text: "var(--container-text)" };
  return (
    <div className={className} style={{ maxWidth: widths[size], margin: "0 auto", padding: "0 var(--gutter)", ...style }}>
      {children}
    </div>
  );
}

function Section({ children, tone = "cream", size = "wide", py = "var(--section-y)", style, id }) {
  const bg = { cream: "var(--color-bg)", sand: "var(--warm-sand)", surface: "var(--color-surface)", ink: "var(--ink-900)", flame: "var(--gradient-brand)" };
  return (
    <section id={id} style={{ background: bg[tone], padding: `${py} 0`, color: tone === "ink" || tone === "flame" ? "var(--color-text-inverse)" : "var(--color-text)", ...style }}>
      <Container size={size}>{children}</Container>
    </section>
  );
}

/* Image slot: renders `src` when provided, else a warm labelled placeholder. */
function Photo({ ratio = "16 / 10", label = "写真", radius = "var(--radius-lg)", src, alt = "", objectPosition = "center", style }) {
  return (
    <div style={{
      aspectRatio: ratio, borderRadius: radius, overflow: "hidden", position: "relative",
      background: "linear-gradient(135deg, var(--warm-sand), var(--amber-100))",
      border: "1px solid var(--color-border)", ...style,
    }}>
      {src ? (
        <img src={src} alt={alt} loading="lazy" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition }} />
      ) : (
      <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 6, color: "var(--ink-300)" }}>
        <svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9.5" r="1.8"/><path d="M4 18l5-5 4 3 3-3 4 4" strokeLinecap="round" strokeLinejoin="round"/></svg>
        <span style={{ fontFamily: "var(--font-en-ui)", fontSize: 11, letterSpacing: ".14em", fontWeight: 700 }}>{label}</span>
      </div>
      )}
    </div>
  );
}

/* Big rotated ghost watermark word (Sarucrew-style side label). */
function GhostWord({ children, style }) {
  return (
    <span aria-hidden="true" style={{
      position: "absolute", fontFamily: "var(--font-en-display)", fontWeight: 800, textTransform: "uppercase",
      color: "var(--ink-900)", opacity: 0.045, fontSize: "clamp(4rem, 3rem + 6vw, 9rem)",
      lineHeight: 1, whiteSpace: "nowrap", pointerEvents: "none", zIndex: 0, ...style,
    }}>{children}</span>
  );
}

/* Scroll-reveal wrapper: fades + rises into view once. Respects reduced-motion. */
function Reveal({ children, delay = 0, y = 28, style, as = "div", className }) {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) { setShown(true); return; }
    // Fallback: never leave content hidden if IO's initial callback doesn't arrive.
    const fallback = setTimeout(() => setShown(true), 450);
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting) { clearTimeout(fallback); setShown(true); io.disconnect(); } });
    }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
    io.observe(el);
    return () => { clearTimeout(fallback); io.disconnect(); };
  }, []);
  const Tag = as;
  return (
    <Tag ref={ref} className={className} style={{
      opacity: shown ? 1 : 0,
      transform: shown ? "translateY(0)" : `translateY(${y}px)`,
      transition: `opacity 0.7s var(--ease-out) ${delay}ms, transform 0.7s var(--ease-out) ${delay}ms`,
      ...style,
    }}>{children}</Tag>
  );
}

/* Editorial section marker: big serial number + English label + rule (Sarucrew-style). */
function Marker({ no, en, invert = false }) {
  const line = invert ? "rgba(255,255,255,0.28)" : "var(--line-strong)";
  const label = invert ? "rgba(255,255,255,0.9)" : "var(--ink-900)";
  return (
    <div style={{ display: "flex", alignItems: "center", gap: "var(--space-4)", marginBottom: "var(--space-6)" }}>
      <span style={{ fontFamily: "var(--font-en-ui)", fontWeight: 800, fontSize: "0.8125rem", letterSpacing: ".14em", background: "var(--gradient-brand)", WebkitBackgroundClip: "text", backgroundClip: "text", WebkitTextFillColor: "transparent" }}>{no}</span>
      <span style={{ fontFamily: "var(--font-en-ui)", fontWeight: 700, fontSize: "0.8125rem", letterSpacing: ".24em", color: label, textTransform: "uppercase" }}>{en}</span>
      <span style={{ flex: 1, height: 1, background: line }} />
    </div>
  );
}

/* Hero video slot. Plays `src` (autoplay/loop/muted/inline) when set; otherwise
   shows a branded placeholder. The user can DRAG A VIDEO FILE onto it to preview
   the vibe instantly (session only — set `src` to a real file for production).
   `fill` makes it a full-bleed background (absolute inset:0, cover, no radius). */
function VideoSlot({ src, poster, ratio = "4 / 5", radius = "var(--radius-xl)", label = "動画をドラッグ", fill = false, style }) {
  const [dropped, setDropped] = React.useState(null);
  const [over, setOver] = React.useState(false);
  const videoSrc = dropped || src;

  const onDrop = (e) => {
    e.preventDefault(); setOver(false);
    const file = e.dataTransfer.files && e.dataTransfer.files[0];
    if (file && file.type.startsWith("video/")) setDropped(URL.createObjectURL(file));
  };

  const frame = fill
    ? { position: "absolute", inset: 0, borderRadius: 0, overflow: "hidden", background: "var(--ink-900)", border: over ? "2px solid var(--flame-to)" : "none" }
    : { aspectRatio: ratio, borderRadius: radius, overflow: "hidden", position: "relative", background: "var(--ink-900)", border: over ? "2px solid var(--flame-to)" : "1px solid var(--color-border)", boxShadow: "var(--shadow-lg)" };

  return (
    <div
      onDragOver={(e) => { e.preventDefault(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={onDrop}
      style={{ ...frame, ...style }}
    >
      {videoSrc ? (
        <video
          src={videoSrc} poster={poster}
          autoPlay loop muted playsInline
          style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
        />
      ) : (
        <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
          {/* animated brand-gradient wash so the empty state still conveys motion */}
          <div aria-hidden="true" style={{ position: "absolute", inset: "-40%", background: "var(--gradient-brand)", opacity: 0.9, filter: "blur(40px)", animation: "tgFloat 9s var(--ease-soft) infinite alternate" }} />
          <div aria-hidden="true" style={{ position: "absolute", inset: 0, background: "linear-gradient(160deg, rgba(30,26,22,0.15), rgba(30,26,22,0.78))" }} />
          {fill ? (
            /* full-bleed: hero copy sits over the center, so keep the empty-state
               hint out of the way as a small bottom-right chip */
            <div style={{ position: "absolute", left: "var(--gutter)", bottom: "var(--space-7)", display: "inline-flex", alignItems: "center", gap: "0.5rem", padding: "0.45rem 0.8rem", borderRadius: "var(--radius-pill)", background: "rgba(0,0,0,0.28)", backdropFilter: "blur(4px)", border: "1px solid rgba(255,255,255,0.28)", color: "rgba(255,255,255,0.9)" }}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z"/></svg>
              <span style={{ fontFamily: "var(--font-en-ui)", fontSize: "0.625rem", letterSpacing: ".14em", fontWeight: 700 }}>DROP A VIDEO</span>
            </div>
          ) : (
            <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "var(--space-4)", color: "#fff", textAlign: "center", padding: "var(--space-6)" }}>
              <span style={{ width: 62, height: 62, borderRadius: "var(--radius-pill)", background: "rgba(255,255,255,0.14)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid rgba(255,255,255,0.4)" }}>
                <svg width="22" height="22" viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z"/></svg>
              </span>
              <div>
                <div style={{ fontFamily: "var(--font-jp-heading)", fontWeight: 700, fontSize: "1rem", letterSpacing: ".04em" }}>{label}</div>
                <div style={{ fontFamily: "var(--font-en-ui)", fontSize: "0.6875rem", letterSpacing: ".14em", opacity: 0.75, marginTop: 6 }}>DROP A VIDEO TO PREVIEW</div>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { Container, Section, Photo, GhostWord, Reveal, Marker, VideoSlot, ParticleText });

/* Speee-style particle hero on a light field: ambient dots drift across the
   whole canvas while the centre pool assembles into each scene word, holds,
   scatters, and reforms into the next — auto-cycling. Flame gold→red tint.
   `onScene(i)` fires on each change so captions can sync. Static under
   reduced-motion. */
function ParticleText({ scenes = ["TEN GROW"], interval = 3400, onScene, style }) {
  const wrapRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const scenesRef = React.useRef(scenes);
  scenesRef.current = scenes;
  const cbRef = React.useRef(onScene);
  cbRef.current = onScene;

  React.useEffect(() => {
    const wrap = wrapRef.current, canvas = canvasRef.current;
    if (!wrap || !canvas) return;
    const ctx = canvas.getContext("2d");
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let W = 0, H = 0, core = [], ambient = [], raf = 0, t0 = performance.now();
    let mouse = { x: -9999, y: -9999 }, sceneI = 0, lastSwitch = performance.now();

    const lerpColor = (r, alpha = 1) => {
      const a = [251, 166, 6], b = [236, 21, 11]; // gold → red (brand flame)
      const c = a.map((v, i) => Math.round(v + (b[i] - v) * r));
      return `rgba(${c[0]},${c[1]},${c[2]},${alpha})`;
    };

    function targetsFor(text) {
      const off = document.createElement("canvas");
      off.width = W; off.height = H;
      const o = off.getContext("2d");
      o.clearRect(0, 0, W, H);
      o.fillStyle = "#fff";
      o.textAlign = "center";
      o.textBaseline = "middle";
      let fontSize = Math.max(54, Math.min(W * 0.16, 230));
      o.font = `800 ${fontSize}px Manrope, sans-serif`;
      // fit long words inside the canvas so nothing is clipped at the edges
      const maxW = W * 0.82;
      const measured = o.measureText(text).width;
      if (measured > maxW) { fontSize = fontSize * (maxW / measured); o.font = `800 ${fontSize}px Manrope, sans-serif`; }
      o.fillText(text, W / 2, H / 2);
      const data = o.getImageData(0, 0, W, H).data;
      const gap = Math.max(4, Math.round(W / 320));
      const pts = [];
      for (let y = 0; y < H; y += gap) {
        for (let x = 0; x < W; x += gap) {
          if (data[(y * W + x) * 4 + 3] > 128) pts.push({ x, y });
        }
      }
      return pts;
    }

    function assign(text, scatter) {
      const targets = targetsFor(text);
      const prev = core.map((p) => ({ x: p.x, y: p.y }));
      core = targets.map((tp, i) => {
        const seed = prev.length ? prev[Math.floor(Math.random() * prev.length)] : { x: Math.random() * W, y: Math.random() * H };
        const p = {
          tx: tp.x, ty: tp.y,
          x: seed.x + (Math.random() - 0.5) * 30,
          y: seed.y + (Math.random() - 0.5) * 30,
          vx: 0, vy: 0,
          r0: 0.85 + Math.random() * 1.25,
          col: lerpColor(tp.x / W, 0.9),
          ph: Math.random() * Math.PI * 2,
          sp: 0.85 + Math.random() * 0.35,
        };
        p.r = p.r0;
        if (scatter) { const a = Math.random() * Math.PI * 2, s = 6 + Math.random() * 12; p.vx = Math.cos(a) * s; p.vy = Math.sin(a) * s; }
        return p;
      });
      if (reduce) core.forEach((p) => { p.x = p.tx; p.y = p.ty; });
    }

    function buildAmbient() {
      const n = Math.round((W * H) / 14000);
      ambient = Array.from({ length: n }, () => ({
        x: Math.random() * W, y: Math.random() * H,
        r: 0.6 + Math.random() * 2.6,
        a: 0.12 + Math.random() * 0.5,
        col: Math.random(),
        vx: (Math.random() - 0.5) * 0.25, vy: (Math.random() - 0.5) * 0.25,
        ph: Math.random() * Math.PI * 2,
      }));
    }

    function init() {
      const rect = wrap.getBoundingClientRect();
      W = Math.max(1, Math.floor(rect.width));
      H = Math.max(1, Math.floor(rect.height));
      canvas.width = W * dpr; canvas.height = H * dpr;
      canvas.style.width = W + "px"; canvas.style.height = H + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      buildAmbient();
      assign(scenesRef.current[sceneI] || "", false);
      if (reduce) draw(0);
    }

    function draw() {
      ctx.clearRect(0, 0, W, H);
      for (const p of ambient) {
        ctx.globalAlpha = p.a;
        ctx.fillStyle = lerpColor(p.col, 1);
        ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
      }
      for (const p of core) {
        ctx.globalAlpha = 0.95;
        ctx.fillStyle = p.col;
        ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    function tick(now) {
      const t = (now - t0) / 1000;
      // scene auto-cycle
      if (scenesRef.current.length > 1 && now - lastSwitch > interval) {
        lastSwitch = now;
        sceneI = (sceneI + 1) % scenesRef.current.length;
        assign(scenesRef.current[sceneI], true);
        if (cbRef.current) cbRef.current(sceneI);
      }
      for (const p of ambient) {
        p.x += p.vx; p.y += p.vy;
        p.a += Math.sin(t * 1.5 + p.ph) * 0.004;
        if (p.x < -6) p.x = W + 6; else if (p.x > W + 6) p.x = -6;
        if (p.y < -6) p.y = H + 6; else if (p.y > H + 6) p.y = -6;
      }
      for (const p of core) {
        p.vx += (p.tx - p.x) * 0.007 * p.sp;
        p.vy += (p.ty - p.y) * 0.007 * p.sp;
        p.vx += Math.cos(p.ph + t * 1.3) * 0.01;
        p.vy += Math.sin(p.ph + t * 1.3) * 0.01;
        const dx = p.x - mouse.x, dy = p.y - mouse.y, d2 = dx * dx + dy * dy;
        if (d2 < 8000) { const f = (8000 - d2) / 8000 * 0.9; const d = Math.sqrt(d2) + 0.01; p.vx += dx / d * f; p.vy += dy / d * f; }
        p.vx *= 0.88; p.vy *= 0.88;
        p.x += p.vx; p.y += p.vy;
      }
      draw();
      raf = requestAnimationFrame(tick);
    }

    function burst(cx, cy) {
      for (const p of core) {
        const dx = p.x - cx, dy = p.y - cy, d = Math.sqrt(dx * dx + dy * dy) + 0.01;
        const f = Math.min(24, 3800 / (d + 26));
        p.vx += dx / d * f; p.vy += dy / d * f;
      }
    }

    let ro;
    const start = () => {
      init();
      if (!reduce) raf = requestAnimationFrame(tick);
      ro = new ResizeObserver(() => { cancelAnimationFrame(raf); init(); if (!reduce) raf = requestAnimationFrame(tick); });
      ro.observe(wrap);
    };
    const onMove = (e) => { const rect = wrap.getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top; };
    const onLeave = () => { mouse.x = -9999; mouse.y = -9999; };
    const onDown = (e) => { const rect = wrap.getBoundingClientRect(); burst(e.clientX - rect.left, e.clientY - rect.top); };
    wrap.addEventListener("pointermove", onMove);
    wrap.addEventListener("pointerleave", onLeave);
    wrap.addEventListener("pointerdown", onDown);

    if (document.fonts && document.fonts.ready) document.fonts.ready.then(start); else start();

    return () => { cancelAnimationFrame(raf); if (ro) ro.disconnect(); wrap.removeEventListener("pointermove", onMove); wrap.removeEventListener("pointerleave", onLeave); wrap.removeEventListener("pointerdown", onDown); };
  }, [interval]);

  return (
    <div ref={wrapRef} aria-label={scenes.join(", ")} style={{ position: "absolute", inset: 0, cursor: "pointer", ...style }}>
      <canvas ref={canvasRef} style={{ display: "block", width: "100%", height: "100%" }} />
    </div>
  );
}
