/* global React */
// Shared UI atoms.

const { useEffect, useRef, useState } = React;

/* ============================================================
 Wordmark — concentric circles + 이온랩
 ============================================================ */
function Wordmark({ size = 17 }) {
 return (
 <span className="wordmark" style={{ fontSize: size }}>
 <span className="mark" aria-hidden>
 <span className="outer">
 <span className="inner" />
 </span>
 </span>
 <span>이온랩</span>
 </span>
 );
}

/* ============================================================
 Reveal — IntersectionObserver fade with fallbacks
 ============================================================ */
function Reveal({ children, delay = 0, as: As = "div", className = "", style = {} }) {
 const ref = useRef(null);
 useEffect(() => {
 const el = ref.current;
 if (!el) return;
 let done = false;
 const reveal = () => { if (!done) { done = true; el.classList.add("in"); } };
 const rect = el.getBoundingClientRect();
 const vh = window.innerHeight || document.documentElement.clientHeight;
 if (rect.top < vh * 0.95 && rect.bottom > 0) { reveal(); return; }

 let io;
 if (typeof IntersectionObserver !== "undefined") {
 io = new IntersectionObserver(
 (entries) => entries.forEach((e) => e.isIntersecting && (reveal(), io && io.unobserve(el))),
 { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
 );
 io.observe(el);
 }
 const onScroll = () => {
 const r = el.getBoundingClientRect();
 if (r.top < (window.innerHeight || 0) * 0.95 && r.bottom > 0) { reveal(); window.removeEventListener("scroll", onScroll); }
 };
 window.addEventListener("scroll", onScroll, { passive: true });
 const t = setTimeout(reveal, 1200);
 return () => { io && io.disconnect(); window.removeEventListener("scroll", onScroll); clearTimeout(t); };
 }, []);
 return (
 <As ref={ref} className={`reveal ${className}`} style={{ "--reveal-delay": `${delay}ms`, ...style }}>
 {children}
 </As>
 );
}

/* ============================================================
 Eyebrow & section head
 ============================================================ */
function Eyebrow({ children }) { return <div className="eyebrow">{children}</div>; }

function SectionHead({ tag, title, children, align = "row" }) {
 if (align === "center") {
 return (
 <div style={{ textAlign: "center", maxWidth: 900, margin: "0 auto clamp(40px, 6vw, 80px)" }}>
 {tag && <div style={{ marginBottom: 28 }}><Eyebrow>{tag}</Eyebrow></div>}
 <h2 style={{ margin: "0 auto" }}>{title}</h2>
 {children && <p className="lede" style={{ marginTop: 24, marginInline: "auto", textAlign: "center" }}>{children}</p>}
 </div>
 );
 }
 return (
 <div className="section-head">
 <div className="tag"><Eyebrow>{tag}</Eyebrow></div>
 <div>
 <h2>{title}</h2>
 {children && <p className="lede" style={{ marginTop: 20 }}>{children}</p>}
 </div>
 </div>
 );
}

/* ============================================================
 Button
 ============================================================ */
function Btn({ children, variant = "primary", size, arrow = false, onClick, type = "button", href }) {
 const cls = `btn ${variant === "primary" ? "" : variant} ${size ? size : ""}`.trim();
 const inner = (<>{children}{arrow && <span className="arr" aria-hidden>→</span>}</>);
 if (href) return <a className={cls} href={href} onClick={onClick}>{inner}</a>;
 return <button type={type} className={cls} onClick={onClick}>{inner}</button>;
}

/* ============================================================
 Animated count up (with viewport fallback)
 ============================================================ */
function CountUp({ to, decimals = 0, prefix = "", suffix = "", duration = 1400 }) {
 const ref = useRef(null);
 const [val, setVal] = useState(0);
 useEffect(() => {
 const el = ref.current;
 if (!el) return;
 let raf, done = false;
 const start = () => {
 if (done) return;
 done = true;
 const t0 = performance.now();
 const step = (t) => {
 const k = Math.min(1, (t - t0) / duration);
 const eased = 1 - Math.pow(1 - k, 3);
 setVal(to * eased);
 if (k < 1) raf = requestAnimationFrame(step);
 };
 raf = requestAnimationFrame(step);
 };
 const rect = el.getBoundingClientRect();
 const vh = window.innerHeight || document.documentElement.clientHeight;
 if (rect.top < vh * 0.95 && rect.bottom > 0) { start(); return () => raf && cancelAnimationFrame(raf); }

 let io;
 if (typeof IntersectionObserver !== "undefined") {
 io = new IntersectionObserver(
 (entries) => entries.forEach((e) => e.isIntersecting && (start(), io && io.unobserve(el))),
 { threshold: 0.4 }
 );
 io.observe(el);
 }
 const onScroll = () => {
 const r = el.getBoundingClientRect();
 if (r.top < (window.innerHeight || 0) * 0.95 && r.bottom > 0) { start(); window.removeEventListener("scroll", onScroll); }
 };
 window.addEventListener("scroll", onScroll, { passive: true });
 const fb = setTimeout(start, 1400);
 return () => { io && io.disconnect(); window.removeEventListener("scroll", onScroll); clearTimeout(fb); if (raf) cancelAnimationFrame(raf); };
 }, [to, duration]);
 const formatted = val.toLocaleString("ko-KR", {
 minimumFractionDigits: decimals, maximumFractionDigits: decimals,
 });
 return <span ref={ref} className="numeric">{prefix}{formatted}{suffix}</span>;
}

/* ============================================================
 VideoPlaceholder — ambient animated gradient mesh as video stand-in
 ============================================================ */
function VideoPlaceholder({ label = "VIDEO PLACEHOLDER", ratio, fill = false, radius, src, style = {} }) {
 const s = {
 ...(fill ? { position: "absolute", inset: 0, width: "100%", height: "100%", borderRadius: 0 } : {}),
 ...(ratio ? { aspectRatio: ratio } : {}),
 ...(radius !== undefined ? { borderRadius: radius } : {}),
 ...style,
 };
 if (src) {
 return (
 <div className="video-ph" style={{ ...s, overflow: "hidden" }} role="img" aria-label={label}>
 <img
 src={src}
 alt={label}
 style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
 />
 </div>
 );
 }
 return (
 <div className="video-ph" style={s} aria-label={label} role="img">
 <div className="mesh" />
 <div className="grain" />
 <div className="label">{label}</div>
 </div>
 );
}

/* ============================================================
 ScrollWords — scroll-driven word-by-word color transition
 ============================================================ */
function ScrollWords({ text, className = "", style = {}, accentWords = [] }) {
 const ref = useRef(null);
 const wordsRef = useRef([]);
 useEffect(() => {
 const el = ref.current;
 if (!el) return;
 const words = el.querySelectorAll(".sw-word");
 wordsRef.current = words;

 const update = () => {
 const rect = el.getBoundingClientRect();
 const vh = window.innerHeight || document.documentElement.clientHeight;
 // progress: 0 when block enters from bottom (rect.top === vh), 1 when block fully passed centerline
 const start = vh * 0.85; // start activating when top crosses 85% vh
 const end = vh * 0.20; // fully active when top crosses 20% vh
 const range = start - end;
 const raw = (start - rect.top) / range;
 const p = Math.max(0, Math.min(1, raw));
 const reveal = Math.ceil(p * words.length);
 words.forEach((w, i) => {
 if (i < reveal) w.classList.add("active");
 else w.classList.remove("active");
 });
 };
 update();
 window.addEventListener("scroll", update, { passive: true });
 window.addEventListener("resize", update);
 return () => {
 window.removeEventListener("scroll", update);
 window.removeEventListener("resize", update);
 };
 }, [text]);

 const tokens = String(text).split(/(\s+)/);
 return (
 <p ref={ref} className={className} style={style}>
 {tokens.map((tok, i) => {
 if (/^\s+$/.test(tok)) return <React.Fragment key={i}>{tok}</React.Fragment>;
 const stripped = tok.replace(/[.,!?;:—]/g, "");
 const isAccent = accentWords.includes(stripped);
 return (
 <span key={i} className={`sw-word ${isAccent ? "accent" : ""}`}>
 {tok}
 </span>
 );
 })}
 </p>
 );
}

/* ============================================================
 Tabs / Accordion / ImgSlot — unchanged
 ============================================================ */
function Tabs({ value, onChange, items }) {
 return (
 <div className="tabs" role="tablist">
 {items.map((it) => (
 <button key={it.value} className="tab" role="tab"
 aria-selected={value === it.value}
 onClick={() => onChange(it.value)}>
 {it.label}
 </button>
 ))}
 </div>
 );
}

function Accordion({ items }) {
 const [open, setOpen] = useState(items[0]?.id ?? null);
 return (
 <div>
 {items.map((it) => (
 <div className="acc-item" key={it.id} data-open={open === it.id}>
 <button className="acc-head" onClick={() => setOpen(open === it.id ? null : it.id)}>
 <span>{it.title}</span>
 <span className="plus" aria-hidden>
 <svg width="14" height="14" viewBox="0 0 14 14">
 <path d="M7 1v12M1 7h12" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
 </svg>
 </span>
 </button>
 <div className="acc-body">
 <div className="acc-body-inner">{it.body}</div>
 </div>
 </div>
 ))}
 </div>
 );
}

function ImgSlot({ label, ratio = "16/10", style = {} }) {
 return (
 <div style={{
 aspectRatio: ratio,
 background: "repeating-linear-gradient(135deg, var(--bg-sub) 0 12px, transparent 12px 24px), var(--bg-elev)",
 border: "1px solid var(--line)",
 borderRadius: "var(--r-lg)",
 display: "grid", placeItems: "center",
 color: "var(--ink-3)",
 fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase",
 ...style,
 }}>{label || "image"}</div>
 );
}

Object.assign(window, {
 Wordmark, Reveal, Eyebrow, SectionHead, Btn, CountUp,
 VideoPlaceholder, ScrollWords, Tabs, Accordion, ImgSlot,
});
