// scenes.jsx — the ScreenSync capture→sync→Figma motion piece.
const { useRef, useEffect } = React;

const STAGE_W = 1080, STAGE_H = 1200;
const CX = STAGE_W / 2;
const BOTTOM_PILL_Y = 1120, TOP_PILL_Y = 70;
const HUB_Y = 1118;

/* helpers */
const ease = Easing;
// triangular pulse 0→1→0
function pulse(t, center, rise, fall) {
  if (t < center - rise || t > center + fall) return 0;
  if (t <= center) return ease.easeOutQuad((t - (center - rise)) / rise);
  return 1 - ease.easeInQuad((t - center) / fall);
}
// quick flash spike: fast up, slow down
function spike(t, center, up = 0.05, down = 0.32, peak = 0.92) {
  if (t < center - up || t > center + down) return 0;
  if (t <= center) return peak * ((t - (center - up)) / up);
  return peak * (1 - ease.easeOutQuad((t - center) / down));
}

/* ───────── persistent background ───────── */
function Background() {
  return null;
}

/* ───────── intro ───────── */
function IntroScene() {
  const { localTime: t } = useSprite();
  const inB = ease.easeOutBack(clamp(t / 0.7, 0, 1));
  const markScale = 0.5 + 0.5 * inB;
  const wordOp = clamp((t - 0.4) / 0.5, 0, 1);
  const eyebrowOp = clamp((t - 0.8) / 0.5, 0, 1);
  const out = clamp((t - 1.7) / 0.5, 0, 1); // fade up & out
  const grpOp = (t < 0.7 ? inB : 1) * (1 - out);
  const grpY = -out * 60;
  return (
    <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: grpOp, transform: `translateY(${grpY}px)` }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 22 }}>
        <div style={{ transform: `scale(${markScale})`, filter: 'drop-shadow(0 8px 30px rgba(0,122,255,0.45))' }}>
          <ScreenSyncMark size={104} />
        </div>
        <span style={{ fontFamily: 'Geist, sans-serif', fontWeight: 700, fontSize: 76, letterSpacing: '-0.045em', color: '#ededed', opacity: wordOp, transform: `translateX(${(1 - wordOp) * -20}px)` }}>ScreenSync</span>
      </div>
      <div style={{ marginTop: 30, fontFamily: 'Geist Mono, monospace', fontSize: 17, letterSpacing: '0.18em', color: '#5f8fff', opacity: eyebrowOp, textTransform: 'uppercase' }}>
        Capture&nbsp;&nbsp;→&nbsp;&nbsp;Sync&nbsp;&nbsp;→&nbsp;&nbsp;Figma
      </div>
    </div>
  );
}

/* ───────── pills ───────── */
function FloatingPill({ y, icon, label, sub, opacity = 1, pulse: pl = 0 }) {
  return (
    <div style={{ position: 'absolute', left: CX, top: y, transform: 'translate(-50%,-50%)', opacity, zIndex: 8 }}>
      <div style={{ transform: `scale(${1 + pl * 0.06})`, filter: pl ? `drop-shadow(0 0 ${18 * pl}px rgba(0,122,255,${0.7 * pl}))` : 'none' }}>
        <Pill icon={icon} label={label} sub={sub} />
      </div>
    </div>
  );
}

/* counter chip on the right */
function CounterChip({ count, opacity, pop }) {
  return (
    <div style={{ position: 'absolute', right: 70, top: 600, transform: 'translateY(-50%)', opacity, zIndex: 8 }}>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, background: '#0f0f11', border: '1px solid #262626', borderRadius: 18, padding: '16px 18px', boxShadow: '0 10px 30px rgba(0,0,0,0.4)' }}>
        <span style={{ transform: `scale(${1 + pop * 0.18})`, transition: 'none' }}>
          <ScreenSyncMark size={26} />
        </span>
        <span style={{ fontFamily: 'Geist, sans-serif', fontWeight: 700, fontSize: 38, color: '#fff', letterSpacing: '-0.04em', transform: `scale(${1 + pop * 0.12})`, lineHeight: 1 }}>{count}</span>
        <span style={{ fontFamily: 'Geist Mono, monospace', fontSize: 10.5, letterSpacing: '0.08em', color: '#8f8f8f', textTransform: 'uppercase' }}>synced</span>
      </div>
    </div>
  );
}

/* ───────── persistent ScreenSync hub (the relay/中转站) ─────────
   Bigger tag, fixed position, present the whole time. Pulses blue
   each time a capture lands and when it pushes frames to Figma. */
function PersistentHub({ pulses = [], total = 8.1 }) {
  const time = useTime();
  const exit = MEase.inOut(clamp((time - (total - 0.5)) / 0.5, 0, 1));
  const op = clamp(time / 0.5, 0, 1) * (1 - exit);
  let pl = 0;
  for (let i = 0; i < pulses.length; i++) {
    const p = pulses[i];
    pl = Math.max(pl, pulse(time, p[0], p[1], p[2]));
  }
  return (
    <div style={{ position: 'absolute', left: CX, top: HUB_Y, transform: 'translate(-50%,-50%)', opacity: op, zIndex: 10 }}>
      <div style={{ transform: `scale(${1.55 * (1 + pl * 0.05)})`, transformOrigin: 'center', filter: pl ? `drop-shadow(0 0 ${24 * pl}px rgba(0,122,255,${0.8 * pl}))` : 'drop-shadow(0 12px 32px rgba(0,0,0,0.45))' }}>
        <Pill icon={<ScreenSyncMark size={22} />} label="ScreenSync" />
      </div>
    </div>
  );
}

/* flying captured thumbnail: phone → ScreenSync hub (straight line, fast) */
function FlyThumb({ variant, t }) {
  if (t <= 0.001 || t >= 1) return null;
  const e = MEase.inOut(t); // accelerates away, decelerates into the hub
  const x = CX;
  const y = interpolate([0, 1], [540, BOTTOM_PILL_Y - 6])(e);
  const sc = interpolate([0, 0.2, 1], [0.6, 0.52, 0.06], MEase.inOut)(t);
  const op = t < 0.82 ? 1 : 1 - MEase.out(clamp((t - 0.82) / 0.18, 0, 1));
  return (
    <div style={{ position: 'absolute', left: x, top: y, transform: `translate(-50%,-50%) scale(${sc})`, zIndex: 7, opacity: op, filter: 'drop-shadow(0 0 26px rgba(0,122,255,0.55))', willChange: 'transform' }}>
      <FrameThumb variant={variant} w={150} radius={18} ring="none" />
    </div>
  );
}

/* ───────── CAPTURE scene (phone + all 3 captures) ───────── */
function CaptureScene() {
  const { localTime: t } = useSprite();

  // entrance / exit of whole rig — quick settle, then straight into capturing
  const enter = MEase.panel(clamp(t / 0.55, 0, 1));
  const enterOp = MEase.out(clamp(t / 0.42, 0, 1));
  const exit = MEase.inOut(clamp((t - 3.95) / 0.35, 0, 1));
  const rigOp = enterOp * (1 - exit);
  const riseY = (1 - enter) * 120 + exit * -90;

  // phone scale is CONSTANT through all captures (settles on entrance, then holds
  // with a barely-perceptible breath so the focal moment has weight).
  const phoneScale = (1.08 + 0.08 * enter) * breathScale(t, (t - 0.7) / 0.6, 0.004);

  // capture beats — start shortly after the phone lands, 1.0s cadence
  const taps = [1.0, 2.0, 3.0];
  const flashes = taps.map((c) => c + 0.07);

  // current screen variant (swap completes before each tap so the captured frame matches)
  let variant = 'travel';
  if (t >= 2.4) variant = 'fitness'; else if (t >= 1.4) variant = 'music';
  // gentle crossfade swap (no scale bounce)
  const swapBlink = Math.max(pulse(t, 1.4, 0.18, 0.22), pulse(t, 2.4, 0.18, 0.22));
  const screenOp = 1 - swapBlink * 0.62;

  // side key self-presses — fast tier: quick press-in, springy release
  const buttonPress = taps.reduce((a, c) => a + clickPulse(t, c, 0.11, 0.26), 0);
  const flash = Math.max(...flashes.map((c) => spike(t, c, 0.04, 0.26)));
  const screenShrink = Math.max(...flashes.map((c) => pulse(t, c + 0.03, 0.1, 0.3)));

  // flying captured thumbnails (phone → hub) — med tier, uniform 0.6s flight
  const fly1 = clamp((t - 1.15) / 0.6, 0, 1);
  const fly2 = clamp((t - 2.15) / 0.6, 0, 1);
  const fly3 = clamp((t - 3.15) / 0.6, 0, 1);

  // sync glow on the phone (fires as each capture lands in the hub)
  const syncPulse = Math.max(pulse(t, 1.75, 0.1, 0.5), pulse(t, 2.75, 0.1, 0.5), pulse(t, 3.75, 0.1, 0.5));

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      {/* phone rig (no hand — the side key animates on its own) */}
      <div style={{ position: 'absolute', left: CX, top: 552, transform: `translate(-50%,-50%) translateY(${riseY}px) scale(${phoneScale * 1.18})`, opacity: rigOp, transformOrigin: 'center', willChange: 'transform' }}>
        <div style={{ opacity: screenOp }}>
          <Phone variant={variant} buttonPress={buttonPress} flash={flash} screenShrink={screenShrink} glow={syncPulse * 0.5} />
        </div>
      </div>

      {/* flying captured thumbnails */}
      <FlyThumb variant="travel" t={fly1} />
      <FlyThumb variant="music" t={fly2} />
      <FlyThumb variant="fitness" t={fly3} />
    </div>
  );
}

/* ───────── LIBRARY scene ───────── */
function LibraryScene() {
  const { localTime: t, duration } = useSprite();
  const inP = ease.easeOutCubic(clamp(t / 0.6, 0, 1));
  const exit = clamp((t - (duration - 0.6)) / 0.6, 0, 1);
  const winOp = inP * (1 - exit);
  const winScale = (0.9 + 0.1 * inP) * (1 - exit * 0.06);
  const winY = (1 - inP) * 60 + exit * -50;

  // cards drop in, staggered
  const cp = {
    travel: ease.easeOutCubic(clamp((t - 0.5) / 0.6, 0, 1)),
    music: ease.easeOutCubic(clamp((t - 0.75) / 0.6, 0, 1)),
    fitness: ease.easeOutCubic(clamp((t - 1.0) / 0.6, 0, 1)),
  };
  // selection sweep then settle on one
  let selected = null;
  if (t > 2.6 && t < 3.2) selected = 'travel';
  else if (t > 3.2 && t < 3.6) selected = 'music';
  else if (t >= 3.6) selected = 'fitness';

  const topPillOp = inP * (1 - exit);
  const figmaPillOp = clamp((t - 2.8) / 0.5, 0, 1) * (1 - exit);

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      <FloatingPill y={TOP_PILL_Y} icon={<ScreenSyncMark size={20} />} label="ScreenSync" sub="organize" opacity={topPillOp} />
      <div style={{ position: 'absolute', left: CX, top: 590, transform: `translate(-50%,-50%) translateY(${winY}px) scale(${winScale})`, opacity: winOp, willChange: 'transform' }}>
        <LibraryWindow captureProgress={cp} selected={selected} syncedCount={3} />
      </div>
      <FloatingPill y={BOTTOM_PILL_Y} icon={<FigmaMark size={18} />} label="Figma" sub="sync →" opacity={figmaPillOp} />
    </div>
  );
}

/* a captured frame "spit out" of the hub onto the (zoomed) canvas — straight, fast */
function EmitThumb({ variant, tx, ty, t, endScale = 1, exitFade = true }) {
  if (t <= 0.001 || t >= 1) return null;
  const e = MEase.outLong(t); // decelerates gently into its canvas spot
  const x = interpolate([0, 1], [CX, tx])(e);
  const y = interpolate([0, 1], [HUB_Y - 28, ty])(e);
  const sc = interpolate([0, 0.2, 1], [0.05, 0.34 * endScale, endScale], MEase.outLong)(t);
  const op = 1; // pure morph — no fade in/out
  return (
    <div style={{ position: 'absolute', left: x, top: y, transform: `translate(-50%,-50%) scale(${sc})`, zIndex: 7, opacity: op, filter: 'drop-shadow(0 0 24px rgba(0,122,255,0.5))', willChange: 'transform' }}>
      <FrameThumb variant={variant} w={150} radius={6} ring="none" />
    </div>
  );
}

/* ───────── FIGMA scene ─────────
   Opens ALREADY zoomed, camera locked on frame 1's landing spot.
   Each frame flies in to the centred spot; once it lands, the camera
   pans to the next spot, then the next frame flies in. */
function FigmaScene() {
  const { localTime: t, duration } = useSprite();
  const inP = MEase.panel(clamp(t / 0.4, 0, 1)); // window settles w/ slight overshoot
  const inOp = MEase.out(clamp(t / 0.32, 0, 1));
  const exit = MEase.inOut(clamp((t - (duration - 0.55)) / 0.55, 0, 1));
  const winOp = inOp * (1 - exit);
  const winScale = (0.93 + 0.07 * inP) * (1 - exit * 0.06);
  const winY0 = (1 - inP) * 40;

  // camera: focused from the start; pans between landing spots
  const amt = MEase.out(clamp(t / 0.35, 0, 1)); // focused almost immediately
  const cs = 1 + 0.72 * amt;
  // pan: each glide STARTS the instant the prior frame lands and finishes ~0.25s
  // before the next thumb arrives — the camera leads the drop so slots never stack
  const camFcx = interpolate([0.0, 1.1, 1.4, 1.65, 1.95], [131, 131, 305, 305, 479], MEase.inOut)(t);

  // frames drop in rapid succession — 0.55s apart over a 0.6s flight (just-touching)
  const emit = {
    travel: clamp((t - 0.5) / 0.6, 0, 1),
    music: clamp((t - 1.05) / 0.6, 0, 1),
    fitness: clamp((t - 1.6) / 0.6, 0, 1),
  };
  // every frame lands at the (zoomed) canvas viewport centre
  const CAN_CX = 615, CAN_CY = 564;

  // frame appears at FULL size the instant its thumb lands — quick opacity handoff,
  // no scale-up pop (the flying thumb fades to reveal the already-placed frame)
  // travel (first) frame appears with a hard handoff (no cross-fade); the others keep the quick opacity handoff
  // each frame is a HARD step at the instant its flying thumb completes its morph —
  // the thumb has morphed (pos+size) to identical geometry, so the swap is invisible:
  // a continuous deformation with no cut, no fade.
  const fp = {
    travel: t >= 1.1 ? 1 : 0,
    music: t >= 1.65 ? 1 : 0,
    fitness: t >= 2.2 ? 1 : 0,
  };

  let focus = null;
  if (t < 1.65) focus = 'travel';
  else if (t < 2.2) focus = 'music';
  else focus = 'fitness';

  return (
    <div style={{ position: 'absolute', inset: 0, overflow: 'hidden' }}>
      <div style={{ position: 'absolute', left: CX, top: 552, transform: `translate(-50%,-50%) translateY(${winY0 + exit * -30}px) scale(${winScale * 1.35})`, opacity: winOp, transformOrigin: 'center', willChange: 'transform' }}>
        <FigmaWindow frameProgress={fp} focus={focus} cam={{ amt, fcx: camFcx }} />
      </div>
      {/* frames spit out of the hub onto the centred (zoomed) canvas spot */}
      <EmitThumb variant="travel" tx={CAN_CX} ty={CAN_CY} t={emit.travel} endScale={cs} exitFade={false} />
      <EmitThumb variant="music" tx={CAN_CX} ty={CAN_CY} t={emit.music} endScale={cs} />
      <EmitThumb variant="fitness" tx={CAN_CX} ty={CAN_CY} t={emit.fitness} endScale={cs} />
    </div>
  );
}

/* ───────── CLOSE scene ───────── */
function CloseScene() {
  const { localTime: t } = useSprite();
  const inP = ease.easeOutBack(clamp(t / 0.7, 0, 1));
  const markScale = 0.6 + 0.4 * inP;
  const lineOp = clamp((t - 0.5) / 0.6, 0, 1);
  const ctaOp = clamp((t - 1.0) / 0.5, 0, 1);
  const ctaScale = ease.easeOutBack(clamp((t - 1.0) / 0.5, 0, 1));
  return (
    <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ position: 'absolute', top: '36%', left: '50%', transform: 'translate(-50%,-50%)', width: 720, height: 560, background: 'radial-gradient(ellipse at center, rgba(0,122,255,0.18), transparent 62%)', filter: 'blur(6px)' }} />
      <div style={{ display: 'flex', alignItems: 'center', gap: 20, opacity: clamp(t / 0.5, 0, 1) }}>
        <div style={{ transform: `scale(${markScale})`, filter: 'drop-shadow(0 8px 30px rgba(0,122,255,0.45))' }}>
          <ScreenSyncMark size={84} />
        </div>
        <span style={{ fontFamily: 'Geist, sans-serif', fontWeight: 700, fontSize: 62, letterSpacing: '-0.045em', color: '#ededed' }}>ScreenSync</span>
      </div>
      <div style={{ marginTop: 28, fontFamily: 'Geist, sans-serif', fontWeight: 600, fontSize: 34, letterSpacing: '-0.03em', color: '#fff', opacity: lineOp, transform: `translateY(${(1 - lineOp) * 14}px)`, textAlign: 'center' }}>
        Your design library, finally in sync.
      </div>
      <div style={{ marginTop: 34, opacity: ctaOp, transform: `scale(${0.9 + 0.1 * ctaScale})` }}>
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, background: '#007aff', color: '#fff', fontFamily: 'Geist, sans-serif', fontWeight: 600, fontSize: 19, padding: '15px 28px', borderRadius: 999, boxShadow: '0 10px 30px rgba(0,122,255,0.45)' }}>
          Start syncing
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M5 12h14M13 6l6 6-6 6" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
        </div>
      </div>
    </div>
  );
}

/* ───────── timestamp labeller (for commenting) ───────── */
function TimeLabeller({ rootRef }) {
  const time = useTime();
  useEffect(() => {
    if (rootRef.current) rootRef.current.setAttribute('data-screen-label', `t=${time.toFixed(0)}s`);
  }, [Math.floor(time)]);
  return null;
}

/* dev seek hook */
function DevSeek() {
  const tl = useTimeline();
  useEffect(() => {
    window.__seek = (t) => { tl.setPlaying(false); tl.setTime(t); };
    window.__play = () => tl.setPlaying(true);
  }, [tl]);
  return null;
}

/* ───────── root ───────── */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "globalSpeed": 1
}/*EDITMODE-END*/;

const CAP_BASE = 4.3;   // capture scene length
const FIG_BASE = 3.6;   // figma scene length
const PAUSE = 0.2;      // brief hand-off between phone and figma

function App() {
  const rootRef = useRef(null);
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);

  const capEnd = CAP_BASE;
  const figStart = capEnd + PAUSE;
  const figEnd = figStart + FIG_BASE;
  const total = figEnd;

  // hub glow pulses (global time): captures landing + figma frames spitting out
  const hubPulses = [
    [1.75, 0.1, 0.45],
    [2.75, 0.1, 0.45],
    [3.75, 0.1, 0.45],
    [figStart + 0.5, 0.09, 0.42],
    [figStart + 1.05, 0.09, 0.42],
    [figStart + 1.6, 0.09, 0.42],
  ];

  return (
    <div ref={rootRef} data-screen-label="t=0s" style={{ position: 'absolute', inset: 0 }}>
      <Stage width={STAGE_W} height={STAGE_H} duration={total} speed={tw.globalSpeed} background="transparent" persistKey="screensync-mg14" chrome={false}>
        <Background />
        <TimeLabeller rootRef={rootRef} />
        <DevSeek />
        <PersistentHub pulses={hubPulses} total={total} />
        <Sprite start={0} end={capEnd}><CaptureScene /></Sprite>
        <Sprite start={figStart} end={figEnd}><FigmaScene /></Sprite>
      </Stage>

      <TweaksPanel title="Tweaks">
        <TweakSection label="速度 · Speed" />
        <TweakSlider label="整体速度" value={tw.globalSpeed} min={0.5} max={2} step={0.05} unit="×" onChange={(v) => setTweak('globalSpeed', v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
