// primitives.jsx — shared monochrome primitives
// Palette: warm off-white bg, near-black text, warm grays.

const PAL = {
  bg:      '#F3F0E8',   // slightly warmer than #FAFAFA for presence on video
  ink:     '#111111',
  sub:     '#5E5A52',
  mute:    '#8A857A',
  ghost:   '#BDB7A8',
  line:    '#D9D3C3',
  paper:   '#ECE8DD',
  deep:    '#0A0A0A',
};

const FONT = {
  serif: "'Instrument Serif', Georgia, serif",
  sans:  "'IBM Plex Sans', system-ui, sans-serif",
  mono:  "'IBM Plex Mono', ui-monospace, monospace",
  kanji: "'Noto Serif JP', 'Instrument Serif', serif",
};

// A single circle that can act as room, task, phase, or idea marker.
// Renders via SVG so strokes stay crisp at any scale.
function Dot({ cx, cy, r, fill = 'none', stroke = PAL.ink, sw = 1.5, opacity = 1, dash }) {
  return (
    <circle cx={cx} cy={cy} r={r}
      fill={fill}
      stroke={stroke}
      strokeWidth={sw}
      strokeDasharray={dash}
      opacity={opacity}
    />
  );
}

// Eyebrow label, uppercase letter-spaced sans.
function Eyebrow({ x, y, text, color = PAL.sub, size = 14, align = 'left' }) {
  const tx = align === 'center' ? '-50%' : align === 'right' ? '-100%' : '0';
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: `translateX(${tx})`,
      fontFamily: FONT.sans, fontWeight: 500,
      fontSize: size, letterSpacing: '0.22em',
      textTransform: 'uppercase', color,
      whiteSpace: 'pre',
    }}>{text}</div>
  );
}

// Thin rule with optional animated draw via scaleX.
function Rule({ x, y, w, color = PAL.line, sw = 1, origin = 'left', scale = 1, opacity = 1 }) {
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      width: w, height: sw, background: color,
      opacity,
      transform: `scaleX(${scale})`,
      transformOrigin: origin === 'left' ? '0 50%' : origin === 'right' ? '100% 50%' : '50% 50%',
    }} />
  );
}

// Words that animate in per-word with stagger, using spring overshoot.
// Pass string. Whitespace is preserved.
function StaggerWords({
  x, y, text, size = 120, color = PAL.ink,
  font = FONT.serif, italic = false, weight = 400,
  lh = 1.02, tracking = '-0.02em',
  stagger = 0.08, entryDur = 0.55, exitDur = 0.4,
  align = 'left', maxWidth,
}) {
  const { localTime, duration } = useSprite();
  const exitStart = Math.max(0, duration - exitDur);
  const exiting = localTime > exitStart;
  const exitT = exiting
    ? Easing.easeInCubic(clamp((localTime - exitStart) / exitDur, 0, 1))
    : 0;

  const words = text.split(/(\s+)/);
  const tx = align === 'center' ? '-50%' : align === 'right' ? '-100%' : '0';

  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: `translateX(${tx})`,
      fontFamily: font,
      fontStyle: italic ? 'italic' : 'normal',
      fontWeight: weight,
      fontSize: size,
      letterSpacing: tracking,
      color,
      lineHeight: lh,
      maxWidth,
      textAlign: align,
      whiteSpace: 'pre-wrap',
    }}>
      {words.map((w, i) => {
        if (/^\s+$/.test(w)) return <span key={i}>{w}</span>;
        const wStart = i * stagger;
        const t = Easing.easeOutBack(clamp((localTime - wStart) / entryDur, 0, 1));
        const op = exiting ? (1 - exitT) : clamp((localTime - wStart) / entryDur, 0, 1);
        const ty = (1 - t) * 34 + exitT * -14;
        const blur = Math.max(0, 8 * (1 - t)) + exitT * 4;
        return (
          <span key={i} style={{
            display: 'inline-block',
            transform: `translateY(${ty}px)`,
            opacity: op,
            filter: `blur(${blur}px)`,
            willChange: 'transform, opacity, filter',
          }}>{w}</span>
        );
      })}
    </div>
  );
}

// Mono number that counts from `from` to `to` during the sprite window,
// then holds. Great for stat reveals.
function CountUp({
  x, y, from = 0, to = 100,
  size = 180, color = PAL.ink,
  weight = 300,
  dur = 1.4, delay = 0,
  suffix = '', prefix = '',
  align = 'left', format = (n) => Math.round(n).toLocaleString(),
  opacity,
}) {
  const { localTime, duration } = useSprite();
  const exitStart = Math.max(0, duration - 0.5);
  const t = Easing.easeOutCubic(clamp((localTime - delay) / dur, 0, 1));
  const val = from + (to - from) * t;
  const op = opacity != null ? opacity :
    clamp((localTime - delay) / 0.25, 0, 1) *
    (localTime > exitStart ? (1 - (localTime - exitStart) / 0.5) : 1);
  const tx = align === 'center' ? '-50%' : align === 'right' ? '-100%' : '0';
  return (
    <div style={{
      position: 'absolute', left: x, top: y,
      transform: `translateX(${tx})`,
      fontFamily: FONT.mono, fontWeight: weight,
      fontSize: size, color,
      letterSpacing: '-0.02em',
      fontVariantNumeric: 'tabular-nums',
      opacity: op,
      whiteSpace: 'pre',
    }}>
      {prefix}{format(val)}{suffix}
    </div>
  );
}

// Ambient background: warm paper with very subtle vignette + grain.
function PaperBG() {
  return (
    <>
      <div style={{
        position: 'absolute', inset: 0,
        background: PAL.bg,
      }}/>
      <div style={{
        position: 'absolute', inset: 0,
        background: 'radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.08) 100%)',
        pointerEvents: 'none',
      }}/>
      <div style={{
        position: 'absolute', inset: 0,
        opacity: 0.035,
        mixBlendMode: 'multiply',
        backgroundImage: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>")`,
        pointerEvents: 'none',
      }}/>
    </>
  );
}

// Sprite-wide fade helper: returns an opacity 0..1..0 factor with optional in/out cushions.
function useSpriteFade({ inDur = 0.4, outDur = 0.4 } = {}) {
  const { localTime, duration } = useSprite();
  const outStart = Math.max(0, duration - outDur);
  const inOp = Easing.easeOutCubic(clamp(localTime / inDur, 0, 1));
  const outOp = localTime > outStart
    ? 1 - Easing.easeInCubic(clamp((localTime - outStart) / outDur, 0, 1))
    : 1;
  return inOp * outOp;
}

Object.assign(window, {
  PAL, FONT, Dot, Eyebrow, Rule, StaggerWords, CountUp, PaperBG, useSpriteFade,
});
