"use client";

import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
  ArrowRightLeft,
  AudioLines,
  ChevronDown,
  ChevronUp,
  Copy,
  Film,
  Image as IconeDeImagem,
  Maximize2,
  MoreHorizontal,
  Music2,
  Pause,
  Play,
  Plus,
  Scissors,
  Shapes,
  SkipBack,
  Sparkles,
  Trash2,
  Type,
  Unlink2,
  Volume2,
  VolumeX,
  ZoomIn,
  ZoomOut,
  Diamond,
} from "lucide-react";
import { DURACAO_MAXIMA, animacaoNova, nomeDaEnfase, nomeDoEfeito, segundos, type AnimacaoDaPeca } from "@/lib/banner-editor/animacao";
import { duracaoDaPagina, fadesDoAudio, intervaloDoClipe, temposDoProjeto, unidadesDaCena, type RelogioDaCena, type UnidadeDaCena } from "@/lib/banner-editor/cena";
import { LIMITE_DE_PAGINAS, numeracaoDosPosts, velocidadeDo, type AudioDoProjeto, type DocumentoDoBanner, type Peca, type PecaDeImagem } from "@/lib/banner-editor/documento";
import { ondaDoVideo, ondaJaMedida } from "@/lib/banner-editor/onda-do-video";
import { Onda } from "./folha-de-audios";
import type { AcaoNaPagina } from "./paginas";
import { Miniatura } from "./paineis";

/*
 * A linha do tempo do projeto, embaixo da arte (o jeito do CapCut): a régua
 * de segundos com a agulha; a trilha das CENAS (cada página com a duração
 * dela, a transição entre uma e outra e o + no fim) e, aberta, uma trilha
 * por peça da cena aberta, com o clipe de quando ela aparece. A entrada e a
 * saída aparecem como as pontas mais fortes do clipe.
 *
 * Arrastar o clipe muda quando a peça entra; as pontas aparam (no vídeo, a
 * da esquerda pula o começo do arquivo). Tudo gruda na agulha, no começo e
 * no fim da cena e nas pontas dos outros clipes. Arrastar a ponta da cena
 * muda a duração. A régua e a agulha mostram a arte naquele instante.
 */

const CABECA = 128;
const RESPIRO = 14;
const ALTURA_DA_TRILHA = 34;
/* A faixa do som do vídeo, logo embaixo do clipe dele (como no CapCut). */
const ALTURA_DO_SOM = 24;
const COR_DO_SOM = { fundo: "#CCFBF1", borda: "#5EEAD4", forte: "#0D9488", texto: "#115E59" };
const IMA = 8;
const CHAVE_DA_ALTURA = "ktalogu:altura-da-linha-do-tempo";

/** "0:02,4" */
export function relogioDoTempo(t: number) {
  const s = Math.max(0, t);
  const m = Math.floor(s / 60);
  const resto = s - m * 60;
  return `${m}:${String(Math.floor(resto)).padStart(2, "0")},${Math.floor((resto * 10) % 10)}`;
}

const um = (v: number) => Math.round(v * 10) / 10;

type TipoDoClipe = "texto" | "imagem" | "video" | "forma";
const CORES: Record<TipoDoClipe, { fundo: string; borda: string; forte: string; texto: string }> = {
  texto: { fundo: "#EDE7FF", borda: "#C4B5FD", forte: "#8B5CF6", texto: "#4C1D95" },
  imagem: { fundo: "#E0F2FE", borda: "#7DD3FC", forte: "#0EA5E9", texto: "#075985" },
  video: { fundo: "#FFEDD5", borda: "#FDBA74", forte: "#F97316", texto: "#9A3412" },
  forma: { fundo: "#DCFCE7", borda: "#86EFAC", forte: "#22C55E", texto: "#166534" },
};
const ICONES: Record<TipoDoClipe, typeof Type> = { texto: Type, imagem: IconeDeImagem, video: Film, forma: Shapes };

function tipoDoClipe(pecas: Peca[]): TipoDoClipe {
  if (pecas.some((p) => p.tipo === "imagem" && p.video)) return "video";
  if (pecas.every((p) => p.tipo === "texto")) return "texto";
  if (pecas.some((p) => p.tipo === "imagem")) return "imagem";
  return "forma";
}

type Trilha = { unidade: UnidadeDaCena; pecas: Peca[]; tipo: TipoDoClipe; video: PecaDeImagem | null; nome: string };

/* O que está sendo arrastado e como fica (a prévia antes de soltar). */
type Arrasto =
  | { tipo: "clipe"; trilha: number; parte: "barra" | "comeco" | "fim"; x0: number; antes: AnimacaoDaPeca; corte0: number; agora: AnimacaoDaPeca; corte: number; guia: number | null }
  | { tipo: "cena"; indice: number; x0: number; antes: number; agora: number }
  | { tipo: "audio"; id: string; parte: "barra" | "comeco" | "fim"; x0: number; antes: AudioDoProjeto; agora: AudioDoProjeto; guia: number | null };

const COR_DO_AUDIO: Record<AudioDoProjeto["tipo"], { fundo: string; borda: string; forte: string; texto: string }> = {
  musica: { fundo: "#FCE7F3", borda: "#F9A8D4", forte: "#DB2777", texto: "#831843" },
  efeito: { fundo: "#FEF3C7", borda: "#FCD34D", forte: "#D97706", texto: "#78350F" },
  enviado: { fundo: "#CFFAFE", borda: "#67E8F9", forte: "#0891B2", texto: "#155E75" },
};

export type AcaoDaLinha = "dividir" | "duplicar" | "apagar" | "efeitos" | "transicao" | "video" | "som" | "audios";
/* Os quadros-chave (tempos na cena aberta). */
export type AcaoNoQuadro = { tipo: "marcar" } | { tipo: "ir"; id: string; t: number } | { tipo: "mover"; id: string; de: number; para: number } | { tipo: "apagar"; id: string; t: number };

export function LinhaDoTempo({
  paginas,
  atual,
  relogio,
  tocando,
  aberta,
  aoAbrir,
  selecionadas,
  aoTocar,
  aoPausar,
  aoBuscar,
  aoMoverAgulha,
  aoIrPagina,
  aoEscolher,
  aoMudarClipe,
  aoMudarDuracao,
  aoAgir,
  aoAgirNaPagina,
  aoAdicionarPagina,
  aoAbrirEfeitos,
  aoAbrirTransicao,
  audios,
  audioEscolhido,
  aoEscolherAudio,
  aoMudarAudio,
  aoQuadro,
  aoVelocidade,
  aoSomDoVideo,
  aoSepararAudio,
}: {
  paginas: DocumentoDoBanner[];
  atual: number;
  relogio: RelogioDaCena;
  tocando: boolean;
  /* As trilhas das peças à mostra (o botão de recolher fecha). */
  aberta: boolean;
  aoAbrir: (v: boolean) => void;
  selecionadas: string[];
  aoTocar: () => void;
  aoPausar: () => void;
  /* Mostrar a arte no instante (prévia) ou só levar a agulha para lá. */
  aoBuscar: (t: number) => void;
  aoMoverAgulha: (t: number) => void;
  aoIrPagina: (i: number) => void;
  aoEscolher: (id: string | null) => void;
  aoMudarClipe: (ids: string[], a: AnimacaoDaPeca | null, video?: { id: string; corte: number } | null) => void;
  aoMudarDuracao: (indice: number, duracao: number) => void;
  aoAgir: (acao: AcaoDaLinha) => void;
  aoAgirNaPagina: (i: number, acao: AcaoNaPagina) => void;
  aoAdicionarPagina: () => void;
  aoAbrirEfeitos: () => void;
  aoAbrirTransicao: (indice: number) => void;
  /* A trilha de áudio do projeto (músicas, efeitos sonoros, áudios enviados). */
  audios: AudioDoProjeto[];
  audioEscolhido: string | null;
  aoEscolherAudio: (id: string | null) => void;
  aoMudarAudio: (id: string, campos: Partial<AudioDoProjeto>) => void;
  aoQuadro: (acao: AcaoNoQuadro) => void;
  /* A velocidade do vídeo escolhido (0 congela). */
  aoVelocidade: (v: number) => void;
  /* O som de um vídeo da cena: volume (0 a 1) e mudo. */
  aoSomDoVideo: (id: string, campos: { volume?: number; mudo?: boolean }) => void;
  /* "Separar áudio": o som do vídeo vira uma faixa própria na trilha de áudio. */
  aoSepararAudio: (id: string) => void;
}) {
  const { inicios, duracoes, total } = useMemo(() => temposDoProjeto(paginas), [paginas]);
  const pagina = paginas[atual];
  const duracao = pagina ? duracaoDaPagina(pagina) : 0;
  const numeros = useMemo(() => numeracaoDosPosts(paginas), [paginas]);
  const trilhas: Trilha[] = useMemo(() => {
    if (!pagina) return [];
    const porId = new Map(pagina.pecas.map((p) => [p.id, p]));
    /* A de cima primeiro, como as camadas. */
    return unidadesDaCena(pagina)
      .filter((u) => !u.fundo)
      .reverse()
      .map((u) => {
        const pecas = u.ids.map((id) => porId.get(id)!).filter(Boolean);
        const video = (pecas.find((p) => p.tipo === "imagem" && p.video) as PecaDeImagem | undefined) ?? null;
        return { unidade: u, pecas, tipo: tipoDoClipe(pecas), video, nome: u.nome };
      });
  }, [pagina]);

  /* A forma de onda do som de cada vídeo da cena (medida uma vez por arquivo). */
  const [ondas, setOndas] = useState<Record<string, number[] | null>>({});
  const videosDaCena = useMemo(() => trilhas.flatMap((t) => (t.video?.video ? [t.video.video] : [])), [trilhas]);
  useEffect(() => {
    let viva = true;
    for (const v of videosDaCena) {
      const ja = ondaJaMedida(v.src);
      if (ja !== undefined) {
        if (!(v.src in ondas)) setOndas((m) => ({ ...m, [v.src]: ja }));
        continue;
      }
      void ondaDoVideo(v.src, v.duracao).then((onda) => {
        if (viva && onda !== undefined) setOndas((m) => ({ ...m, [v.src]: onda }));
      });
    }
    return () => {
      viva = false;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- `ondas` só evita pôr de novo o que já está
  }, [videosDaCena]);

  /* ------------------------------------------------------------- zoom */
  const rolagem = useRef<HTMLDivElement | null>(null);
  const [largura, setLargura] = useState(900);
  /* Tela baixa: menos trilhas à mostra (a arte precisa de espaço); tela estreita: os botões só com o ícone. */
  const [alturaDaJanela, setAlturaDaJanela] = useState(900);
  /* A altura que a pessoa escolheu arrastando a borda de cima (null = a de sempre); fica lembrada neste navegador. */
  const [alturaEscolhida, setAlturaEscolhida] = useState<number | null>(null);
  useEffect(() => {
    const medir = () => setAlturaDaJanela(window.innerHeight);
    medir();
    const lembrar = () => {
      try {
        const guardada = Number(window.localStorage.getItem(CHAVE_DA_ALTURA));
        if (guardada > 0) setAlturaEscolhida(guardada);
      } catch {
        /* sem armazenamento: começa no tamanho de sempre */
      }
    };
    lembrar();
    window.addEventListener("resize", medir);
    return () => window.removeEventListener("resize", medir);
  }, []);
  const trilhasAMostra = alturaDaJanela < 800 ? 1.6 : alturaDaJanela < 940 ? 2.6 : 3.6;
  const SO_AS_CENAS = 28 + 62 + 6;
  const alturaPadrao = aberta ? 28 + 62 + ALTURA_DA_TRILHA * trilhasAMostra + 10 : SO_AS_CENAS;
  const alturaMaxima = Math.max(SO_AS_CENAS + 40, alturaDaJanela * 0.72 - 110);
  const alturaDasTrilhas = alturaEscolhida !== null && aberta ? Math.max(SO_AS_CENAS, Math.min(alturaMaxima, alturaEscolhida)) : alturaPadrao;
  const guardarAltura = (h: number | null) => {
    try {
      if (h === null) window.localStorage.removeItem(CHAVE_DA_ALTURA);
      else window.localStorage.setItem(CHAVE_DA_ALTURA, String(Math.round(h)));
    } catch {
      /* sem armazenamento, só não lembra */
    }
  };
  /* A alça da borda de cima: arrastar para cima aumenta a linha do tempo, para baixo diminui; dois cliques voltam ao normal. */
  const arrastarBorda = (e: React.PointerEvent) => {
    if (e.button !== 0) return;
    e.preventDefault();
    const y0 = e.clientY;
    const h0 = alturaDasTrilhas;
    let ultima = h0;
    if (!aberta) aoAbrir(true);
    document.body.style.cursor = "ns-resize";
    const mover = (ev: PointerEvent) => {
      ultima = Math.max(SO_AS_CENAS, Math.min(alturaMaxima, h0 + (y0 - ev.clientY)));
      setAlturaEscolhida(ultima);
    };
    const soltar = () => {
      window.removeEventListener("pointermove", mover);
      window.removeEventListener("pointerup", soltar);
      document.body.style.cursor = "";
      guardarAltura(ultima);
    };
    window.addEventListener("pointermove", mover);
    window.addEventListener("pointerup", soltar);
  };
  /* Com o volume do áudio ou o som do vídeo à mostra, os botões também ficam só com o ícone (cabe tudo). */
  const compacta = largura < 1280 || Boolean(audioEscolhido && audios.some((a) => a.id === audioEscolhido));
  const [pps, setPps] = useState<number | null>(null);
  useLayoutEffect(() => {
    const el = rolagem.current;
    if (!el) return;
    const medir = () => setLargura(el.clientWidth);
    medir();
    const obs = new ResizeObserver(medir);
    obs.observe(el);
    return () => obs.disconnect();
  }, []);
  /* Sem zoom escolhido, o projeto inteiro cabe na largura (com o + no fim). O
     conteúdo fica 4 px MENOR que a caixa: com "- 70" ele passava 6 px, a barra
     de rolagem de baixo ficava sempre lá e a linha escorregava para os lados. */
  const caber = Math.max(8, Math.min(240, (largura - CABECA - RESPIRO * 2 - 80) / Math.max(1, total)));
  const escala = pps ?? caber;
  const xDe = (t: number) => RESPIRO + t * escala;
  const larguraDoConteudo = xDe(total) + 90;

  /* ------------------------------------------------------------ agulha */
  const agulha = useRef<HTMLDivElement | null>(null);
  const cabecaDaAgulha = useRef<HTMLDivElement | null>(null);
  const textoDoRelogio = useRef<HTMLSpanElement | null>(null);
  const [tempoAgora, setTempoAgora] = useState(0);
  useEffect(
    () =>
      relogio.assinar((t) => {
        const x = `${xDe(t)}px`;
        if (agulha.current) {
          agulha.current.style.transform = `translateX(${x})`;
          agulha.current.style.opacity = xDe(t) < (rolagem.current?.scrollLeft ?? 0) ? "0" : "1";
        }
        if (cabecaDaAgulha.current) cabecaDaAgulha.current.style.transform = `translateX(${x})`;
        if (textoDoRelogio.current) textoDoRelogio.current.textContent = relogioDoTempo(t);
        /* Tocando, a vista acompanha a agulha. */
        const el = rolagem.current;
        if (el && relogio.tocando) {
          const pos = CABECA + xDe(t);
          if (pos > el.scrollLeft + el.clientWidth - 40 || pos < el.scrollLeft + CABECA) el.scrollLeft = pos - CABECA - 60;
        }
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps -- xDe muda com a escala
    [relogio, escala],
  );
  /* O instante parado (para os botões que dependem dele), sem re-renderizar tocando. */
  useEffect(() => {
    if (tocando) return;
    const t = window.setInterval(() => setTempoAgora(relogio.tempo()), 200);
    return () => window.clearInterval(t);
  }, [tocando, relogio]);

  const tempoNoPonto = (clientX: number) => {
    const el = rolagem.current;
    if (!el) return 0;
    const caixa = el.getBoundingClientRect();
    const x = clientX - caixa.left + el.scrollLeft - CABECA - RESPIRO;
    return Math.max(0, Math.min(total, x / escala));
  };

  const arrastarAgulha = (e: React.PointerEvent) => {
    if (e.button !== 0) return;
    e.preventDefault();
    aoBuscar(um(tempoNoPonto(e.clientX)));
    const mover = (ev: PointerEvent) => aoBuscar(um(tempoNoPonto(ev.clientX)));
    const soltar = () => {
      window.removeEventListener("pointermove", mover);
      window.removeEventListener("pointerup", soltar);
    };
    window.addEventListener("pointermove", mover);
    window.addEventListener("pointerup", soltar);
  };

  /* ----------------------------------------------------------- arrastos */
  const [arrasto, setArrasto] = useState<Arrasto | null>(null);
  const arrastoRef = useRef<Arrasto | null>(null);
  const atualizar = (a: Arrasto | null) => {
    arrastoRef.current = a;
    setArrasto(a);
  };

  /* Onde o clipe gruda (segundos da cena): começo, fim, agulha e as pontas dos outros. */
  const imas = useCallback(
    (fora: number) => {
      const lista = [0, duracao];
      const agora = relogio.tempo() - inicios[atual];
      if (agora > 0 && agora < duracao) lista.push(agora);
      trilhas.forEach((t, i) => {
        if (i === fora) return;
        const { comeco, fim } = intervaloDoClipe(t.unidade.animacao, duracao);
        lista.push(comeco, fim);
      });
      return lista;
    },
    [duracao, relogio, inicios, atual, trilhas],
  );
  const grudar = (v: number, lista: number[]) => {
    let melhor: number | null = null;
    for (const m of lista) if (Math.abs(m - v) * escala <= IMA && (melhor === null || Math.abs(m - v) < Math.abs(melhor - v))) melhor = m;
    return melhor;
  };

  /* A animação com o clipe de `comeco` a `fim` (visíveis): fim no fim da cena = fica até o fim. */
  const comIntervalo = (a: AnimacaoDaPeca, comeco: number, fim: number): AnimacaoDaPeca => {
    const saida = a.saida && a.fim !== null ? a.duracaoDaSaida : 0;
    const fimDaPeca = fim >= duracao - 0.05 ? null : um(Math.max(comeco + 0.1, fim - saida));
    return { ...a, inicio: um(comeco), fim: fimDaPeca };
  };

  const comecarClipe = (e: React.PointerEvent, i: number, parte: "barra" | "comeco" | "fim") => {
    if (e.button !== 0) return;
    e.preventDefault();
    e.stopPropagation();
    const t = trilhas[i];
    aoEscolher(t.pecas[0].id);
    const antes = t.unidade.animacao ?? animacaoNova(null, { inicio: 0 });
    const corte0 = t.video?.video?.corte ?? 0;
    atualizar({ tipo: "clipe", trilha: i, parte, x0: e.clientX, antes, corte0, agora: antes, corte: corte0, guia: null });
    const lista = imas(i);
    const mover = (ev: PointerEvent) => {
      const a = arrastoRef.current;
      if (!a || a.tipo !== "clipe") return;
      const dt = (ev.clientX - a.x0) / escala;
      const { comeco: c0, fim: f0 } = intervaloDoClipe(a.antes, duracao);
      const largo = f0 - c0;
      /* Com velocidade, o arquivo rende (duração ÷ velocidade) de clipe; congelado, não acaba. */
      const vel = t.video?.video ? (t.video.video.velocidade ?? 1) : 1;
      const arquivo = t.video?.video && vel > 0 ? t.video.video.duracao / vel : Infinity;
      let comeco = c0;
      let fim = f0;
      let corte = a.corte0;
      let guia: number | null = null;
      if (a.parte === "barra") {
        comeco = Math.max(0, Math.min(duracao - largo, c0 + dt));
        const gc = grudar(comeco, lista);
        const gf = grudar(comeco + largo, lista);
        if (gc !== null) comeco = guia = gc;
        else if (gf !== null) {
          comeco = gf - largo;
          guia = gf;
        }
        comeco = Math.max(0, Math.min(duracao - largo, comeco));
        fim = comeco + largo;
      } else if (a.parte === "comeco") {
        /* No vídeo, a ponta da esquerda não passa do começo do arquivo. */
        const minimo = t.video?.video && vel > 0 ? Math.max(0, c0 - a.corte0 / vel) : 0;
        comeco = Math.max(minimo, Math.min(f0 - 0.2, c0 + dt));
        const g = grudar(comeco, lista);
        if (g !== null && g >= minimo && g <= f0 - 0.2) comeco = guia = g;
        corte = Math.max(0, a.corte0 + (comeco - c0) * vel);
      } else {
        const maximo = Math.min(duracao, c0 + (arquivo - (vel > 0 ? a.corte0 / vel : 0)));
        fim = Math.max(c0 + 0.2, Math.min(maximo, f0 + dt));
        const g = grudar(fim, lista);
        if (g !== null && g >= c0 + 0.2 && g <= maximo) fim = guia = g;
      }
      atualizar({ ...a, agora: comIntervalo(a.antes, comeco, fim), corte: um(corte), guia });
    };
    const soltar = () => {
      window.removeEventListener("pointermove", mover);
      window.removeEventListener("pointerup", soltar);
      const a = arrastoRef.current;
      atualizar(null);
      if (!a || a.tipo !== "clipe") return;
      const mudou = JSON.stringify(a.agora) !== JSON.stringify(a.antes) || a.corte !== a.corte0;
      if (mudou) aoMudarClipe(t.unidade.ids, a.agora, t.video && a.corte !== a.corte0 ? { id: t.video.id, corte: a.corte } : null);
    };
    window.addEventListener("pointermove", mover);
    window.addEventListener("pointerup", soltar);
  };

  const comecarCena = (e: React.PointerEvent, i: number) => {
    if (e.button !== 0) return;
    e.preventDefault();
    e.stopPropagation();
    atualizar({ tipo: "cena", indice: i, x0: e.clientX, antes: duracoes[i], agora: duracoes[i] });
    const mover = (ev: PointerEvent) => {
      const a = arrastoRef.current;
      if (!a || a.tipo !== "cena") return;
      atualizar({ ...a, agora: Math.max(0.5, Math.min(DURACAO_MAXIMA, um(a.antes + (ev.clientX - a.x0) / escala))) });
    };
    const soltar = () => {
      window.removeEventListener("pointermove", mover);
      window.removeEventListener("pointerup", soltar);
      const a = arrastoRef.current;
      atualizar(null);
      if (a && a.tipo === "cena" && a.agora !== a.antes) aoMudarDuracao(i, a.agora);
    };
    window.addEventListener("pointermove", mover);
    window.addEventListener("pointerup", soltar);
  };

  /* O clipe de áudio: anda pelo projeto inteiro (gruda no começo, na agulha, nas cenas e nos outros áudios). */
  const comecarAudio = (e: React.PointerEvent, a: AudioDoProjeto, parte: "barra" | "comeco" | "fim") => {
    if (e.button !== 0) return;
    e.preventDefault();
    e.stopPropagation();
    aoEscolherAudio(a.id);
    atualizar({ tipo: "audio", id: a.id, parte, x0: e.clientX, antes: a, agora: a, guia: null });
    const lista = [0, total, relogio.tempo(), ...inicios, ...audios.filter((x) => x.id !== a.id).flatMap((x) => [x.inicio, x.inicio + x.dura])];
    const mover = (ev: PointerEvent) => {
      const atual = arrastoRef.current;
      if (!atual || atual.tipo !== "audio") return;
      const dt = (ev.clientX - atual.x0) / escala;
      const b = atual.antes;
      let agora: AudioDoProjeto = b;
      let guia: number | null = null;
      if (parte === "barra") {
        let inicio = Math.max(0, Math.min(Math.max(0, total - 0.2), b.inicio + dt));
        const g = grudar(inicio, lista);
        const gf = grudar(inicio + b.dura, lista);
        if (g !== null) inicio = guia = g;
        else if (gf !== null) {
          inicio = Math.max(0, gf - b.dura);
          guia = gf;
        }
        agora = { ...b, inicio: um(inicio) };
      } else if (parte === "comeco") {
        /* A ponta da esquerda não passa do começo do arquivo nem da ponta da direita. */
        let inicio = Math.max(b.inicio - b.corte, Math.min(b.inicio + b.dura - 0.2, b.inicio + dt));
        const g = grudar(inicio, lista);
        if (g !== null && g >= b.inicio - b.corte && g <= b.inicio + b.dura - 0.2) inicio = guia = g;
        inicio = Math.max(0, inicio);
        const andou = inicio - b.inicio;
        agora = { ...b, inicio: um(inicio), corte: um(Math.max(0, b.corte + andou)), dura: um(b.dura - andou) };
      } else {
        let fim = Math.max(b.inicio + 0.2, Math.min(b.inicio + b.duracao - b.corte, b.inicio + b.dura + dt));
        const g = grudar(fim, lista);
        if (g !== null && g >= b.inicio + 0.2 && g <= b.inicio + b.duracao - b.corte) fim = guia = g;
        agora = { ...b, dura: um(fim - b.inicio) };
      }
      atualizar({ ...atual, agora, guia });
    };
    const soltar = () => {
      window.removeEventListener("pointermove", mover);
      window.removeEventListener("pointerup", soltar);
      const atual = arrastoRef.current;
      atualizar(null);
      if (atual && atual.tipo === "audio" && JSON.stringify(atual.agora) !== JSON.stringify(atual.antes)) aoMudarAudio(atual.id, { inicio: atual.agora.inicio, corte: atual.agora.corte, dura: atual.agora.dura });
    };
    window.addEventListener("pointermove", mover);
    window.addEventListener("pointerup", soltar);
  };

  /* O áudio escolhido (ou que acabou de entrar) aparece: a trilha rola até ele. */
  useEffect(() => {
    if (!audioEscolhido) return;
    const el = rolagem.current?.querySelector<HTMLElement>(`[data-trilha-de-audio="${audioEscolhido}"]`);
    const caixa = rolagem.current;
    if (!el || !caixa) return;
    const topo = el.offsetTop - 28 - 62;
    if (topo < caixa.scrollTop || el.offsetTop + el.offsetHeight > caixa.scrollTop + caixa.clientHeight) caixa.scrollTo({ top: Math.max(0, topo - 8), behavior: "smooth" });
  }, [audioEscolhido, audios.length]);

  /* A duração vista de cada cena (com a que está sendo esticada). */
  const duracaoVista = (i: number) => (arrasto?.tipo === "cena" && arrasto.indice === i ? arrasto.agora : duracoes[i]);
  const inicioVisto = (i: number) => inicios.slice(0, i).reduce((s, _, j) => s + duracaoVista(j), 0);

  /* ------------------------------------------------------------ menus */
  /* O painel do som do vídeo (volume, mudo, separar), aberto pelo botão da faixa "Som do vídeo". */
  const [painelDoSom, setPainelDoSom] = useState<{ id: string; x: number; y: number } | null>(null);
  const caixaDoPainelDoSom = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    if (!painelDoSom) return;
    const fechar = (e: PointerEvent) => {
      if (e.target instanceof Node && caixaDoPainelDoSom.current?.contains(e.target)) return;
      if (e.target instanceof Element && e.target.closest(`[data-abrir-som="${painelDoSom.id}"]`)) return;
      setPainelDoSom(null);
    };
    window.addEventListener("pointerdown", fechar, true);
    return () => window.removeEventListener("pointerdown", fechar, true);
  }, [painelDoSom]);
  const [menuDaCena, setMenuDaCena] = useState<number | null>(null);
  useEffect(() => {
    if (menuDaCena === null) return;
    const fechar = () => setMenuDaCena(null);
    window.addEventListener("pointerdown", fechar);
    return () => window.removeEventListener("pointerdown", fechar);
  }, [menuDaCena]);

  const escolhida = trilhas.find((t) => t.pecas.some((p) => selecionadas.includes(p.id))) ?? null;
  const local = tempoAgora - inicios[atual];
  const naCena = local > 0.05 && local < duracao - 0.05;
  const cortaClipe = Boolean(escolhida && (() => {
    const { comeco, fim } = intervaloDoClipe(escolhida.unidade.animacao, duracao);
    return local > comeco + 0.05 && local < fim - 0.05;
  })());
  const marcas = (() => {
    const passo = escala >= 80 ? 0.5 : escala >= 40 ? 1 : escala >= 20 ? 2 : escala >= 10 ? 5 : 10;
    const lista: Array<{ t: number; grande: boolean }> = [];
    for (let t = 0; t <= total + 0.001; t += passo / 2) lista.push({ t: um(t), grande: Math.abs(t / passo - Math.round(t / passo)) < 0.001 });
    return lista;
  })();

  const botao = "flex h-9 items-center gap-1.5 rounded-[9px] px-2.5 text-[12.5px] font-semibold text-[#3B3552] transition hover:bg-[#F1EFF6] disabled:opacity-35 disabled:hover:bg-transparent";
  const icone = "flex h-9 w-9 items-center justify-center rounded-[9px] text-[#3B3552] transition hover:bg-[#F1EFF6] disabled:opacity-35";

  return (
    <section data-linha-do-tempo className="pointer-events-auto relative flex w-full flex-col border-t border-[#E6E3EE] bg-white shadow-[0_-10px_30px_rgba(20,11,43,0.06)]" onPointerDown={(e) => e.stopPropagation()}>
      {/* A alça para subir e descer a linha do tempo (a arte ganha ou perde espaço). */}
      <div
        onPointerDown={arrastarBorda}
        onDoubleClick={() => {
          setAlturaEscolhida(null);
          guardarAltura(null);
        }}
        className="group absolute inset-x-0 -top-[5px] z-50 flex h-[10px] cursor-ns-resize items-center justify-center"
        title="Arraste para aumentar ou diminuir a linha do tempo (dois cliques: tamanho normal)"
        data-alca-da-linha
      >
        <span className="h-[3px] w-full bg-transparent transition group-hover:bg-[#7C3AED]/40" />
        <span className="absolute h-[5px] w-12 rounded-full bg-[#CFC8E2] transition group-hover:bg-[#7C3AED]" />
      </div>
      {/* ------------------------------------------------ barra de ferramentas */}
      <div className="grid h-12 shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 border-b border-[#EFEDF5] px-2.5">
        <div className="flex min-w-0 items-center gap-0.5 overflow-hidden">
        <button type="button" className={botao} onClick={() => aoAgir("dividir")} disabled={!naCena && !audioEscolhido} title={cortaClipe ? "Dividir o clipe escolhido no ponto da agulha (S)" : "Dividir a cena no ponto da agulha (S)"} data-acao-da-linha="dividir">
          <Scissors size={16} /> <span className={compacta ? "sr-only" : ""}>Dividir</span>
        </button>
        <button type="button" className={icone} onClick={() => aoAgir("duplicar")} title={selecionadas.length ? "Duplicar o clipe (Ctrl+D)" : "Duplicar a cena"} data-acao-da-linha="duplicar">
          <Copy size={16} />
        </button>
        <button type="button" className={`${icone} hover:!bg-[#FFE4E6] hover:!text-[#BE123C]`} onClick={() => aoAgir("apagar")} disabled={!selecionadas.length && !audioEscolhido && paginas.length < 2} title={selecionadas.length || audioEscolhido ? "Apagar o clipe (Delete)" : "Apagar a cena"} data-acao-da-linha="apagar">
          <Trash2 size={16} />
        </button>
        <span className="mx-1.5 h-6 w-px bg-[#E6E3EE]" />
        <button type="button" className={botao} onClick={aoAbrirEfeitos} title="Animação: como entra, se mexe e sai" data-acao-da-linha="efeitos">
          <Sparkles size={16} className="text-[#7C3AED]" /> <span className={compacta ? "sr-only" : ""}>Animação</span>
        </button>
        <button
          type="button"
          className={icone}
          onClick={() => aoQuadro({ tipo: "marcar" })}
          disabled={!selecionadas.length}
          title="Quadro-chave na agulha (K): guarda onde a peça está, o tamanho, o giro e a transparência. Depois, em outro instante, mexa na peça e o próximo quadro é gravado sozinho. Clicar de novo no mesmo instante tira."
          data-acao-da-linha="quadro"
        >
          <Diamond size={16} className="text-[#7C3AED]" />
        </button>
        <button type="button" className={botao} onClick={() => aoAbrirTransicao(Math.max(1, atual))} disabled={paginas.length < 2} title={paginas.length < 2 ? "Com duas cenas ou mais, escolha como uma passa para a outra" : "Transição entre as cenas"} data-acao-da-linha="transicao">
          <ArrowRightLeft size={16} /> <span className={compacta ? "sr-only" : ""}>Transição</span>
        </button>
        <button type="button" className={botao} onClick={() => aoAgir("audios")} title="Músicas, efeitos sonoros e o seu áudio" data-acao-da-linha="audios">
          <Music2 size={16} className="text-[#DB2777]" /> <span className={compacta ? "sr-only" : ""}>Áudio</span>
        </button>
        <button type="button" className={botao} onClick={() => aoAgir("video")} title="Pôr um vídeo na cena" data-acao-da-linha="video">
          <Film size={16} /> <span className={compacta ? "sr-only" : ""}>Vídeo</span>
        </button>
        {audios.find((a) => a.id === audioEscolhido) ? (
          (() => {
            const a = audios.find((x) => x.id === audioEscolhido)!;
            return (
              <span className="ml-1 flex items-center gap-1.5 rounded-full bg-[#FDF2F8] py-1 pl-2.5 pr-1.5" data-volume-do-audio>
                {a.volume ? <Volume2 size={14} className="text-[#DB2777]" /> : <VolumeX size={14} className="text-[#BE123C]" />}
                <input type="range" min={0} max={100} value={Math.round(a.volume * 100)} onChange={(e) => aoMudarAudio(a.id, { volume: Number(e.target.value) / 100 })} className="w-20 accent-[#DB2777]" aria-label="Volume do áudio" />
                {(["entrada", "saida"] as const).map((lado, k) => {
                  const valor = Math.round(fadesDoAudio(a)[k] * 10) / 10;
                  const opcoes = [...new Set([0, 0.5, 1, 2, 3, 5, 8, valor])].sort((x, y) => x - y);
                  return (
                    <label key={lado} className="flex items-center gap-1 text-[11px] font-bold text-[#831843]" title={lado === "entrada" ? "Fade in: o som começa baixinho e sobe" : "Fade out: o som desce até sumir no fim do clipe"}>
                      {lado === "entrada" ? "Fade in" : "Fade out"}
                      <select
                        value={valor}
                        onChange={(e) => aoMudarAudio(a.id, lado === "entrada" ? { fadeDeEntrada: Number(e.target.value) } : { fadeDeSaida: Number(e.target.value) })}
                        className="h-6 rounded-full border-0 bg-white px-1 text-[11px] font-bold text-[#831843] outline-none ring-1 ring-[#F9A8D4]"
                        data-fade-do-audio={lado}
                      >
                        {opcoes.map((s) => (
                          <option key={s} value={s}>
                            {s ? segundos(s) : "não"}
                          </option>
                        ))}
                      </select>
                    </label>
                  );
                })}
              </span>
            );
          })()
        ) : null}
        {escolhida?.video?.video ? (
          <select
            value={String(escolhida.video.video.velocidade ?? 1)}
            onChange={(e) => aoVelocidade(Number(e.target.value))}
            title="Velocidade do vídeo: mais lento, mais rápido ou o quadro congelado (dividir o clipe deixa cada parte numa velocidade)"
            className="h-8 rounded-[8px] border-0 bg-[#F3F1F9] px-2 text-[12px] font-bold text-[#4B4760] outline-none hover:bg-[#EAE6F5]"
            data-velocidade-do-video
          >
            {[0, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4].map((v) => (
              <option key={v} value={v}>
                {v === 0 ? "Congelar" : `${String(v).replace(".", ",")}×`}
              </option>
            ))}
          </select>
        ) : null}
        {escolhida?.video?.video ? (
          <button type="button" className={icone} onClick={() => aoAgir("som")} title={escolhida.video.video.mudo ? "Ligar o som do vídeo" : "Tirar o som do vídeo"} data-acao-da-linha="som">
            {escolhida.video.video.mudo ? <VolumeX size={16} className="text-[#BE123C]" /> : <Volume2 size={16} />}
          </button>
        ) : null}

        </div>

        {/* O play no meio, como nos editores de vídeo. */}
        <div className="flex items-center gap-2">
          <button type="button" className={icone} onClick={() => aoMoverAgulha(0)} title="Voltar ao começo" data-voltar-ao-comeco>
            <SkipBack size={16} />
          </button>
          <button type="button" onClick={tocando ? aoPausar : aoTocar} title={tocando ? "Pausar (Espaço)" : "Reproduzir (Espaço)"} className="flex h-10 w-10 items-center justify-center rounded-full bg-[#7C3AED] text-white shadow-[0_4px_14px_rgba(124,58,237,0.35)] transition hover:bg-[#6D28D9]" data-play={tocando ? "tocando" : "parado"}>
            {tocando ? <Pause size={17} fill="currentColor" /> : <Play size={17} fill="currentColor" className="ml-0.5" />}
          </button>
          <span className="min-w-[104px] whitespace-nowrap text-[12.5px] font-semibold tabular-nums text-[#2B2640]">
            <span ref={textoDoRelogio} data-relogio>
              {relogioDoTempo(0)}
            </span>
            <span className="text-[#A39DB8]"> / {relogioDoTempo(total)}</span>
          </span>
        </div>

        <div className="flex min-w-0 items-center justify-end gap-0.5">
          <button type="button" className={icone} onClick={() => setPps(Math.max(6, escala / 1.4))} title="Afastar a linha do tempo">
            <ZoomOut size={16} />
          </button>
          <input type="range" min={6} max={240} value={Math.round(escala)} onChange={(e) => setPps(Number(e.target.value))} className={`${compacta ? "w-16" : "w-24"} accent-[#7C3AED]`} aria-label="Zoom da linha do tempo" data-zoom-da-linha />
          <button type="button" className={icone} onClick={() => setPps(Math.min(240, escala * 1.4))} title="Aproximar a linha do tempo">
            <ZoomIn size={16} />
          </button>
          <button type="button" className={icone} onClick={() => setPps(null)} title="Caber o projeto inteiro">
            <Maximize2 size={15} />
          </button>
          <span className="mx-1 h-6 w-px bg-[#E6E3EE]" />
          <button
            type="button"
            className={icone}
            onClick={() => {
              aoAbrir(!aberta);
              setAlturaEscolhida(null);
              guardarAltura(null);
            }} title={aberta ? "Esconder as trilhas das peças" : "Mostrar as trilhas das peças"} data-abrir-trilhas>
            {aberta ? <ChevronDown size={17} /> : <ChevronUp size={17} />}
          </button>
        </div>
      </div>

      {/* ------------------------------------------------------- as trilhas */}
      <div
        ref={rolagem}
        className="relative overflow-auto [scrollbar-width:thin]"
        style={alturaEscolhida !== null && aberta ? { height: alturaDasTrilhas } : { maxHeight: alturaDasTrilhas }}
        onScroll={() => {
          if (agulha.current) agulha.current.style.opacity = xDe(relogio.tempo()) < (rolagem.current?.scrollLeft ?? 0) ? "0" : "1";
        }}
        onWheel={(e) => {
          /* Ctrl + roda: zoom; Shift + roda: anda para os lados. */
          if (e.ctrlKey) {
            e.preventDefault();
            setPps(Math.max(6, Math.min(240, escala * (e.deltaY < 0 ? 1.15 : 1 / 1.15))));
          } else if (e.shiftKey && rolagem.current) rolagem.current.scrollLeft += e.deltaY;
        }}
      >
        <div className="relative" style={{ width: CABECA + larguraDoConteudo, minWidth: "100%" }}>
          {/* Régua */}
          <div className="sticky top-0 z-40 flex h-7 bg-white">
            <div className="sticky left-0 z-40 flex w-[128px] shrink-0 items-center border-r border-[#EFEDF5] bg-white px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-[#A39DB8]">Tempo</div>
            <div className="relative h-7 flex-1 cursor-ew-resize select-none border-b border-[#EFEDF5]" onPointerDown={arrastarAgulha} data-regua>
              {marcas.map((m) => (
                <span key={m.t} className="absolute bottom-0" style={{ left: xDe(m.t) }}>
                  <span className={`block w-px ${m.grande ? "h-2.5 bg-[#BDB6D3]" : "h-1.5 bg-[#DDD8EA]"}`} />
                  {m.grande ? <span className="absolute bottom-3 -translate-x-1/2 text-[10px] font-semibold tabular-nums text-[#8A849E]">{m.t < 60 ? `${String(m.t).replace(".", ",")}s` : relogioDoTempo(m.t)}</span> : null}
                </span>
              ))}
              <div ref={cabecaDaAgulha} className="pointer-events-none absolute bottom-0 left-0 z-10" style={{ transform: `translateX(${xDe(0)}px)` }}>
                <span className="absolute -left-[6px] bottom-0 h-3.5 w-3 rounded-b-[4px] rounded-t-[2px] bg-[#E11D48]" />
              </div>
            </div>
          </div>

          {/* Cenas */}
          <div className="sticky top-7 z-30 flex h-[62px] bg-white">
            <div className="sticky left-0 z-40 flex w-[128px] shrink-0 items-center gap-2 border-r border-[#EFEDF5] bg-white px-3 text-[12px] font-bold text-[#2B2640]">
              <Film size={14} className="text-[#7C3AED]" /> Cenas
            </div>
            <div className="relative flex-1">
              {paginas.map((p, i) => {
                const x = xDe(inicioVisto(i));
                const w = Math.max(18, duracaoVista(i) * escala);
                const lado = Math.max(20, Math.min(70, (44 * p.largura) / p.altura));
                return (
                  <div key={i} className="group absolute top-[7px]" style={{ left: x, width: w }} data-cena-da-linha={i}>
                    <button
                      type="button"
                      onClick={() => aoIrPagina(i)}
                      onContextMenu={(e) => {
                        e.preventDefault();
                        setMenuDaCena(i);
                      }}
                      className={`flex h-12 w-full items-center gap-2 overflow-hidden rounded-[10px] pl-1 pr-3 text-left transition ${i === atual ? "bg-[#EDE7FF] ring-2 ring-[#7C3AED]" : "bg-[#F4F2F9] ring-1 ring-[#E2DEEC] hover:ring-[#C4B5FD]"}`}
                      title={`Cena ${i + 1}: clique para abrir, arraste a ponta para mudar a duração`}
                    >
                      <span className="pointer-events-none block shrink-0 overflow-hidden rounded-[7px] ring-1 ring-black/10" style={{ width: lado, height: 40 }}>
                        <Miniatura doc={p} largura={lado} contagem={numeros[i]} />
                      </span>
                      {w > lado + 40 ? (
                        <span className="min-w-0 leading-tight">
                          <span className={`block truncate text-[11.5px] font-bold ${i === atual ? "text-[#5B21B6]" : "text-[#3B3552]"}`}>Cena {i + 1}</span>
                          <span className="block text-[10.5px] font-semibold tabular-nums text-[#8A849E]">{relogioDoTempo(duracaoVista(i)).replace(/^0:0?/, "")} s</span>
                        </span>
                      ) : null}
                    </button>
                    {/* A ponta direita estica ou encolhe a cena. */}
                    <span onPointerDown={(e) => comecarCena(e, i)} className="absolute bottom-0 right-0 top-0 flex w-3 cursor-ew-resize items-center justify-center rounded-r-[10px] opacity-0 hover:bg-[#7C3AED]/15 group-hover:opacity-100" data-ponta-da-cena={i} title="Arraste para mudar a duração da cena">
                      <span className="h-5 w-[3px] rounded-full bg-[#7C3AED]" />
                    </span>
                    <button
                      type="button"
                      onPointerDown={(e) => e.stopPropagation()}
                      onClick={(e) => {
                        e.stopPropagation();
                        setMenuDaCena((m) => (m === i ? null : i));
                      }}
                      className="absolute right-3 top-1 flex h-5 w-5 items-center justify-center rounded-[5px] bg-white/95 text-[#4B4760] opacity-0 shadow-sm ring-1 ring-black/10 group-hover:opacity-100"
                      title="Opções da cena"
                    >
                      <MoreHorizontal size={12} />
                    </button>
                    {menuDaCena === i ? (
                      <div className="absolute bottom-[calc(100%+6px)] left-0 z-50 w-[200px] rounded-[12px] bg-white p-1 shadow-[0_18px_50px_rgba(20,11,43,0.25)] ring-1 ring-black/5" onPointerDown={(e) => e.stopPropagation()}>
                        {(
                          [
                            ["duplicar", "Duplicar a cena", paginas.length >= LIMITE_DE_PAGINAS],
                            ["esquerda", "Mover para a esquerda", i === 0],
                            ["direita", "Mover para a direita", i === paginas.length - 1],
                            ["apagar", "Apagar a cena", paginas.length < 2],
                          ] as Array<[AcaoNaPagina, string, boolean]>
                        ).map(([acao, rotulo, desligado]) => (
                          <button
                            key={acao}
                            type="button"
                            disabled={desligado}
                            onClick={() => {
                              setMenuDaCena(null);
                              aoAgirNaPagina(i, acao);
                            }}
                            className={`flex w-full items-center rounded-[8px] px-2.5 py-1.5 text-left text-[12.5px] font-medium text-[#2B2640] hover:bg-[#F3F1F9] disabled:opacity-35 ${acao === "apagar" ? "hover:!bg-[#FFE4E6] hover:!text-[#BE123C]" : ""}`}
                          >
                            {rotulo}
                          </button>
                        ))}
                      </div>
                    ) : null}
                  </div>
                );
              })}
              {/* A transição entre uma cena e a outra. */}
              {paginas.map((p, i) =>
                i === 0 ? null : (
                  <button
                    key={`tr${i}`}
                    type="button"
                    onClick={() => aoAbrirTransicao(i)}
                    className={`absolute top-[19px] z-10 flex h-6 w-6 -translate-x-1/2 items-center justify-center rounded-full shadow-sm ring-2 ring-white transition-colors hover:ring-[#C4B5FD] ${p.transicao ? "bg-[#7C3AED] text-white" : "bg-[#E6E3EE] text-[#6B6584] hover:bg-[#DDD3FF]"}`}
                    style={{ left: xDe(inicioVisto(i)) }}
                    title={p.transicao ? "Mudar a transição" : "Pôr uma transição"}
                    data-transicao-da-linha={i}
                  >
                    <ArrowRightLeft size={11} />
                  </button>
                ),
              )}
              <button
                type="button"
                onClick={aoAdicionarPagina}
                disabled={paginas.length >= LIMITE_DE_PAGINAS}
                className="absolute top-[7px] flex h-12 w-12 items-center justify-center rounded-[10px] border-2 border-dashed border-[#D6D1E6] text-[#6B6584] transition hover:border-[#A78BFA] hover:text-[#6D28D9] disabled:opacity-40"
                style={{ left: xDe(inicioVisto(paginas.length)) + 8 }}
                title="Nova cena (copia o design da cena aberta)"
                data-nova-cena
              >
                <Plus size={18} />
              </button>
            </div>
          </div>

          {/* As peças da cena aberta */}
          {aberta
            ? trilhas.length
              ? trilhas.map((t, i) => {
                  const base = arrasto?.tipo === "clipe" && arrasto.trilha === i ? arrasto.agora : t.unidade.animacao;
                  const { comeco, fim } = intervaloDoClipe(base, duracao);
                  const x = xDe(inicios[atual] + comeco);
                  const w = Math.max(10, (fim - comeco) * escala);
                  const cor = CORES[t.tipo];
                  const Icone = ICONES[t.tipo];
                  const escolhidaAqui = t.pecas.some((p) => selecionadas.includes(p.id));
                  const entrada = base?.entrada ? Math.min(w, base.duracao * escala) : 0;
                  const saida = base && base.fim !== null && base.saida ? Math.min(w - entrada, base.duracaoDaSaida * escala) : 0;
                  const rotulo = [base?.entrada ? nomeDoEfeito(base.entrada) : null, base?.enfase ? nomeDaEnfase(base.enfase) : null, base && base.fim !== null && base.saida ? `sai: ${nomeDoEfeito(base.saida)}` : null].filter(Boolean).join(" · ");
                  /* A foto (ou o logo, ou o vídeo) repetida ao longo do clipe: dá para ver até onde ela fica na tela. */
                  const fundoDoClipe = t.video ? `url("${t.video.src}")` : t.tipo === "imagem" ? `url("${(t.pecas.find((p) => p.tipo === "imagem") as PecaDeImagem | undefined)?.src ?? ""}")` : "";
                  /* O som do vídeo: o pedaço do arquivo que o clipe toca (com o corte e a velocidade). */
                  const doVideo = t.video?.video ?? null;
                  const corteVisto = arrasto?.tipo === "clipe" && arrasto.trilha === i ? arrasto.corte : (doVideo?.corte ?? 0);
                  const ondaDoClipe = doVideo ? ondas[doVideo.src] : undefined;
                  const velDoClipe = doVideo ? velocidadeDo(doVideo) : 1;
                  return (
                    <Fragment key={t.unidade.ids.join(",")}>
                    <div className="flex" style={{ height: ALTURA_DA_TRILHA }} data-trilha={i}>
                      <button type="button" onClick={() => aoEscolher(t.pecas[0].id)} title={t.nome} className={`sticky left-0 z-20 flex w-[128px] shrink-0 items-center gap-1.5 border-r border-[#EFEDF5] px-3 text-left text-[11.5px] font-semibold ${escolhidaAqui ? "bg-[#F6F3FF] text-[#5B21B6]" : "bg-white text-[#4B4760] hover:bg-[#FAF9FC]"}`}>
                        <Icone size={13} style={{ color: cor.forte }} className="shrink-0" />
                        <span className="truncate">{t.nome}</span>
                      </button>
                      <div className="relative flex-1 border-b border-[#F4F2F8]" onPointerDown={(e) => e.target === e.currentTarget && aoEscolher(null)}>
                        {/* Fora da cena aberta: hachurado. */}
                        <span className="pointer-events-none absolute bottom-0 top-0 bg-[repeating-linear-gradient(135deg,#F7F6FA_0_6px,#FFFFFF_6px_12px)]" style={{ left: RESPIRO, width: Math.max(0, xDe(inicios[atual]) - RESPIRO) }} />
                        <span className="pointer-events-none absolute bottom-0 top-0 bg-[repeating-linear-gradient(135deg,#F7F6FA_0_6px,#FFFFFF_6px_12px)]" style={{ left: xDe(inicios[atual] + duracao), width: Math.max(0, xDe(total) - xDe(inicios[atual] + duracao)) }} />
                        {/* Os quadros-chave: losangos em cima do clipe (arrastar muda o instante; clicar leva a agulha; botão direito apaga). */}
                        {(base?.quadros ?? []).map((q) => (
                          <button
                            key={q.t}
                            type="button"
                            className="absolute top-[10px] z-[6] h-[14px] w-[14px] -translate-x-1/2 cursor-ew-resize"
                            style={{ left: xDe(inicios[atual] + q.t) }}
                            title={`Quadro-chave em ${segundos(q.t)}: arraste para mudar o instante, clique para ir até ele, botão direito para apagar`}
                            onPointerDown={(e) => {
                              e.stopPropagation();
                              /* O botão direito é para apagar (onContextMenu), não arrasta. */
                              if (e.button !== 0) return;
                              const x0 = e.clientX;
                              const id = t.pecas[0].id;
                              const alvo = e.currentTarget;
                              alvo.setPointerCapture(e.pointerId);
                              let andou = false;
                              const mover = (ev: PointerEvent) => {
                                if (Math.abs(ev.clientX - x0) > 3) andou = true;
                                alvo.style.transform = `translateX(${ev.clientX - x0}px)`;
                              };
                              const soltar = (ev: PointerEvent) => {
                                alvo.removeEventListener("pointermove", mover);
                                alvo.removeEventListener("pointerup", soltar);
                                alvo.style.transform = "";
                                const para = Math.max(0, Math.min(duracao, q.t + (ev.clientX - x0) / escala));
                                if (andou) aoQuadro({ tipo: "mover", id, de: q.t, para });
                                else aoQuadro({ tipo: "ir", id, t: q.t });
                              };
                              alvo.addEventListener("pointermove", mover);
                              alvo.addEventListener("pointerup", soltar);
                            }}
                            onContextMenu={(e) => {
                              e.preventDefault();
                              aoQuadro({ tipo: "apagar", id: t.pecas[0].id, t: q.t });
                            }}
                            data-quadro-chave={q.t}
                          >
                            <span className="block h-[10px] w-[10px] rotate-45 rounded-[2px] border-[1.5px] border-white bg-[#7C3AED] shadow-[0_1px_3px_rgba(0,0,0,0.35)]" />
                          </button>
                        ))}
                        <div
                          className={`group absolute top-[4px] flex h-[26px] cursor-grab items-center overflow-hidden rounded-[7px] active:cursor-grabbing ${escolhidaAqui ? "ring-2 ring-[#7C3AED] ring-offset-1" : ""}`}
                          style={{ left: x, width: w, background: cor.fundo, boxShadow: `inset 0 0 0 1px ${cor.borda}` }}
                          onPointerDown={(e) => comecarClipe(e, i, "barra")}
                          onDoubleClick={() => {
                            aoEscolher(t.pecas[0].id);
                            aoAbrirEfeitos();
                          }}
                          title={`${t.nome}${rotulo ? ` · ${rotulo}` : ""} — arraste para mudar quando entra; dois cliques para a animação`}
                          data-clipe={i}
                          data-comeco={um(comeco)}
                          data-fim={um(fim)}
                        >
                          {fundoDoClipe ? <span className="pointer-events-none absolute inset-0 opacity-45" style={{ backgroundImage: fundoDoClipe, backgroundSize: "auto 100%", backgroundRepeat: "repeat-x" }} /> : null}
                          {entrada ? <span className="pointer-events-none absolute bottom-0 left-0 top-0" style={{ width: entrada, background: `linear-gradient(90deg, ${cor.forte}AA, ${cor.forte}33)` }} /> : null}
                          {saida ? <span className="pointer-events-none absolute bottom-0 right-0 top-0" style={{ width: saida, background: `linear-gradient(270deg, ${cor.forte}AA, ${cor.forte}33)` }} /> : null}
                          <span className="pointer-events-none relative flex min-w-0 items-center gap-1 px-2.5 text-[10.5px] font-bold" style={{ color: cor.texto }}>
                            {base?.entrada || base?.enfase || base?.saida ? <Sparkles size={10} className="shrink-0" /> : null}
                            <span className="truncate">{w > 70 ? (rotulo || t.nome) : ""}</span>
                          </span>
                          <span onPointerDown={(e) => comecarClipe(e, i, "comeco")} className={`absolute bottom-0 left-0 top-0 w-2.5 cursor-ew-resize ${escolhidaAqui ? "bg-[#7C3AED]" : "bg-transparent group-hover:bg-black/10"}`} data-ponta-comeco={i}>
                            {escolhidaAqui ? <span className="absolute left-[4px] top-1/2 h-3 w-[2px] -translate-y-1/2 rounded bg-white" /> : null}
                          </span>
                          <span onPointerDown={(e) => comecarClipe(e, i, "fim")} className={`absolute bottom-0 right-0 top-0 w-2.5 cursor-ew-resize ${escolhidaAqui ? "bg-[#7C3AED]" : "bg-transparent group-hover:bg-black/10"}`} data-ponta-fim={i}>
                            {escolhidaAqui ? <span className="absolute right-[4px] top-1/2 h-3 w-[2px] -translate-y-1/2 rounded bg-white" /> : null}
                          </span>
                        </div>
                      </div>
                    </div>
                    {doVideo && t.video ? (
                      /* Grudado no vídeo: arrastar aqui leva o vídeo junto. "Separar áudio" solta numa faixa própria. */
                      <div className="flex" style={{ height: ALTURA_DO_SOM }} data-som-do-video={t.video.id}>
                        <div className={`sticky left-0 z-20 flex w-[128px] shrink-0 items-center gap-0.5 border-r border-[#EFEDF5] pl-5 pr-1 text-[11px] font-semibold ${escolhidaAqui ? "bg-[#F0FDFA] text-[#115E59]" : "bg-white text-[#6B6584]"}`}>
                          <button type="button" onClick={() => aoEscolher(t.video!.id)} title="O som do vídeo: anda junto com ele. Para editar à parte, use Separar áudio." className="flex min-w-0 flex-1 items-center gap-1.5 text-left hover:text-[#115E59]" data-escolher-pelo-som>
                            <AudioLines size={12} style={{ color: COR_DO_SOM.forte }} className="shrink-0" />
                            <span className="truncate">Som</span>
                          </button>
                          <button
                            type="button"
                            onClick={(e) => {
                              const r = e.currentTarget.getBoundingClientRect();
                              const id = t.video!.id;
                              setPainelDoSom((atual) => (atual?.id === id ? null : { id, x: r.left, y: r.bottom + 6 }));
                            }}
                            className="flex h-5 w-5 shrink-0 items-center justify-center rounded-[5px] text-[#0F766E] transition hover:bg-[#CCFBF1]"
                            title={doVideo.mudo ? "Vídeo sem som: volume e som" : `Volume do som do vídeo (${Math.round(doVideo.volume * 100)}%)`}
                            data-abrir-som={t.video.id}
                          >
                            {doVideo.mudo ? <VolumeX size={12} className="text-[#BE123C]" /> : <Volume2 size={12} />}
                          </button>
                          <button
                            type="button"
                            onClick={() => aoSepararAudio(t.video!.id)}
                            disabled={doVideo.mudo || ondaDoClipe === null || velDoClipe === 0}
                            className="flex h-5 w-5 shrink-0 items-center justify-center rounded-[5px] text-[#0F766E] transition hover:bg-[#CCFBF1] disabled:opacity-35 disabled:hover:bg-transparent"
                            title={ondaDoClipe === null ? "Este vídeo não tem som" : doVideo.mudo ? "O vídeo está sem som (ligue o som para separar)" : "Separar áudio: o som do vídeo vira uma faixa própria na trilha de áudio (aparar, mover, volume, fade, dividir) e o vídeo fica mudo"}
                            data-separar-audio={t.video.id}
                          >
                            <Unlink2 size={12} />
                          </button>
                        </div>
                        <div className="relative flex-1 border-b border-[#F4F2F8]" onPointerDown={(e) => e.target === e.currentTarget && aoEscolher(null)}>
                          <div
                            className={`absolute top-[2px] flex h-[18px] cursor-grab items-center overflow-hidden rounded-[6px] active:cursor-grabbing ${escolhidaAqui ? "ring-1 ring-[#0D9488]" : ""}`}
                            style={{ left: x, width: w, background: doVideo.mudo || ondaDoClipe === null || velDoClipe === 0 ? "#F4F2F8" : COR_DO_SOM.fundo, boxShadow: `inset 0 0 0 1px ${doVideo.mudo || ondaDoClipe === null ? "#E2DEEC" : COR_DO_SOM.borda}` }}
                            onPointerDown={(e) => comecarClipe(e, i, "barra")}
                            title={doVideo.mudo ? "Vídeo sem som" : ondaDoClipe === null ? "Este vídeo não tem som" : "Som do vídeo — arraste para mover junto com o vídeo"}
                            data-clipe-do-som={i}
                            data-mudo={doVideo.mudo ? "sim" : "nao"}
                          >
                            {doVideo.mudo || ondaDoClipe === null || velDoClipe === 0 ? (
                              <span className="pointer-events-none flex items-center gap-1 px-2 text-[10px] font-bold text-[#8A849E]">
                                <VolumeX size={11} /> {w > 90 ? (ondaDoClipe === null ? "Sem som no arquivo" : velDoClipe === 0 ? "Congelado, sem som" : "Sem som") : ""}
                              </span>
                            ) : ondaDoClipe?.length ? (
                              <span className="pointer-events-none absolute inset-x-1 top-[2px] opacity-70">
                                <Onda onda={ondaDoClipe} cor={COR_DO_SOM.forte} altura={14} de={corteVisto / doVideo.duracao} ate={Math.min(1, (corteVisto + (fim - comeco) * velDoClipe) / doVideo.duracao)} />
                              </span>
                            ) : (
                              <span className="pointer-events-none px-2 text-[10px] font-bold" style={{ color: COR_DO_SOM.texto }}>
                                {w > 90 ? "Som do vídeo" : ""}
                              </span>
                            )}
                          </div>
                        </div>
                      </div>
                    ) : null}
                    </Fragment>
                  );
                })
              : (
                  <p className="px-[140px] py-3 text-[12px] text-[#8A849E]">Esta cena só tem o fundo. Ponha um texto, uma foto ou um vídeo e o clipe aparece aqui.</p>
                )
            : null}

          {/* A trilha de áudio: um clipe por linha, pelo projeto inteiro. */}
          {aberta ? (
            <>
              {audios.map((a0) => {
                const a = arrasto?.tipo === "audio" && arrasto.id === a0.id ? arrasto.agora : a0;
                const cor = COR_DO_AUDIO[a.tipo];
                const escolhidoAqui = audioEscolhido === a.id;
                const w = Math.max(10, a.dura * escala);
                return (
                  <div key={a.id} className="flex" style={{ height: ALTURA_DA_TRILHA }} data-trilha-de-audio={a.id}>
                    <button type="button" onClick={() => aoEscolherAudio(a.id)} title={a.nome} className={`sticky left-0 z-20 flex w-[128px] shrink-0 items-center gap-1.5 border-r border-[#EFEDF5] px-3 text-left text-[11.5px] font-semibold ${escolhidoAqui ? "bg-[#FDF2F8] text-[#9D174D]" : "bg-white text-[#4B4760] hover:bg-[#FAF9FC]"}`}>
                      <Music2 size={13} style={{ color: cor.forte }} className="shrink-0" />
                      <span className="truncate">{a.nome}</span>
                    </button>
                    <div className="relative flex-1 border-b border-[#F4F2F8]" onPointerDown={(e) => e.target === e.currentTarget && aoEscolherAudio(null)}>
                      <div
                        className={`group absolute top-[4px] flex h-[26px] cursor-grab items-center overflow-hidden rounded-[7px] active:cursor-grabbing ${escolhidoAqui ? "ring-2 ring-[#DB2777] ring-offset-1" : ""}`}
                        style={{ left: xDe(a.inicio), width: w, background: cor.fundo, boxShadow: `inset 0 0 0 1px ${cor.borda}` }}
                        onPointerDown={(e) => comecarAudio(e, a0, "barra")}
                        title={`${a.nome} — arraste para mudar quando toca; as pontas aparam`}
                        data-clipe-de-audio={a.id}
                        data-inicio={a.inicio}
                        data-dura={a.dura}
                      >
                        {a.onda?.length ? (
                          <span className="pointer-events-none absolute inset-x-1 top-[3px] opacity-60">
                            <Onda onda={a.onda} cor={cor.forte} altura={20} de={a.corte / a.duracao} ate={(a.corte + a.dura) / a.duracao} />
                          </span>
                        ) : null}
                        <span className="pointer-events-none relative truncate px-2.5 text-[10.5px] font-bold" style={{ color: cor.texto }}>
                          {w > 70 ? a.nome : ""}
                        </span>
                        <span onPointerDown={(e) => comecarAudio(e, a0, "comeco")} className={`absolute bottom-0 left-0 top-0 w-2.5 cursor-ew-resize ${escolhidoAqui ? "bg-[#DB2777]" : "group-hover:bg-black/10"}`} data-ponta-comeco-audio={a.id} />
                        <span onPointerDown={(e) => comecarAudio(e, a0, "fim")} className={`absolute bottom-0 right-0 top-0 w-2.5 cursor-ew-resize ${escolhidoAqui ? "bg-[#DB2777]" : "group-hover:bg-black/10"}`} data-ponta-fim-audio={a.id} />
                      </div>
                    </div>
                  </div>
                );
              })}
              <div className="flex" style={{ height: ALTURA_DA_TRILHA }}>
                <div className="sticky left-0 z-20 flex w-[128px] shrink-0 items-center gap-1.5 border-r border-[#EFEDF5] bg-white px-3 text-[11.5px] font-semibold text-[#A39DB8]">
                  <Music2 size={13} className="shrink-0" /> Áudio
                </div>
                <div className="relative flex-1">
                  <button type="button" onClick={() => aoAgir("audios")} className="absolute top-[4px] flex h-[26px] items-center gap-1.5 rounded-[7px] border border-dashed border-[#F9A8D4] px-3 text-[11px] font-bold text-[#BE185D] transition hover:bg-[#FDF2F8]" style={{ left: xDe(0) }} data-pôr-audio>
                    <Plus size={12} /> {audios.length ? "Mais um som" : "Pôr música ou efeito sonoro"}
                  </button>
                </div>
              </div>
            </>
          ) : null}

          {/* A agulha atravessando tudo; a linha amarela é onde o clipe grudou. */}
          <div ref={agulha} className="pointer-events-none absolute bottom-0 top-0 z-[35] w-0" style={{ left: CABECA, transform: `translateX(${xDe(0)}px)` }} data-agulha>
            <span className="absolute bottom-0 top-0 -left-px w-[2px] bg-[#E11D48]" />
          </div>
          {arrasto?.tipo === "clipe" && arrasto.guia !== null ? <div className="pointer-events-none absolute bottom-0 top-7 z-[34] w-px bg-[#F59E0B]" style={{ left: CABECA + xDe(inicios[atual] + arrasto.guia) }} /> : null}
          {arrasto?.tipo === "audio" && arrasto.guia !== null ? <div className="pointer-events-none absolute bottom-0 top-7 z-[34] w-px bg-[#F59E0B]" style={{ left: CABECA + xDe(arrasto.guia) }} /> : null}
          {arrasto ? (
            <div className="pointer-events-none fixed z-[200] rounded-[6px] bg-[#1F1B2E] px-2 py-1 text-[11px] font-bold tabular-nums text-white" style={{ left: "50%", bottom: 12 }}>
              {arrasto.tipo === "cena" ? `Cena ${arrasto.indice + 1}: ${relogioDoTempo(arrasto.agora)}` : arrasto.tipo === "audio" ? `${relogioDoTempo(arrasto.agora.inicio)} → ${relogioDoTempo(arrasto.agora.inicio + arrasto.agora.dura)}` : (() => {
                const { comeco, fim } = intervaloDoClipe(arrasto.agora, duracao);
                return `${relogioDoTempo(comeco)} → ${relogioDoTempo(fim)}`;
              })()}
            </div>
          ) : null}
        </div>
      </div>
      {painelDoSom
        ? (() => {
            const t = trilhas.find((x) => x.video?.id === painelDoSom.id);
            const v = t?.video?.video;
            if (!t || !v) return null;
            const semSom = ondas[v.src] === null;
            const cima = painelDoSom.y + 150 > window.innerHeight;
            return (
              <div
                ref={caixaDoPainelDoSom}
                className="fixed z-[210] w-[236px] rounded-[12px] bg-white p-3 shadow-[0_18px_50px_rgba(20,11,43,0.25)] ring-1 ring-black/5"
                style={{ left: painelDoSom.x, ...(cima ? { bottom: Math.max(8, window.innerHeight - painelDoSom.y + 36) } : { top: painelDoSom.y }) }}
                data-painel-do-som
              >
                <div className="flex items-center justify-between text-[12px] font-bold text-[#2B2640]">
                  <span className="flex items-center gap-1.5">
                    <AudioLines size={13} style={{ color: COR_DO_SOM.forte }} /> Som do vídeo
                  </span>
                  <span className="tabular-nums text-[#0F766E]">{semSom ? "sem som" : v.mudo ? "mudo" : `${Math.round(v.volume * 100)}%`}</span>
                </div>
                <input
                  type="range"
                  min={0}
                  max={100}
                  value={v.mudo ? 0 : Math.round(v.volume * 100)}
                  disabled={semSom}
                  onChange={(e) => aoSomDoVideo(painelDoSom.id, { volume: Number(e.target.value) / 100 })}
                  className="mt-2 w-full accent-[#0D9488] disabled:opacity-40"
                  aria-label="Volume do som do vídeo"
                  data-volume-do-video
                />
                <div className="mt-2.5 flex gap-1.5">
                  <button type="button" disabled={semSom} onClick={() => aoSomDoVideo(painelDoSom.id, { mudo: !v.mudo })} className="flex h-8 flex-1 items-center justify-center gap-1 rounded-[8px] bg-[#F3F1F9] text-[11.5px] font-bold text-[#3B3552] transition hover:bg-[#EAE6F5] disabled:opacity-40" data-mudo-do-video>
                    {v.mudo ? <Volume2 size={13} /> : <VolumeX size={13} />} {v.mudo ? "Ligar o som" : "Tirar o som"}
                  </button>
                  <button
                    type="button"
                    disabled={semSom || v.mudo || velocidadeDo(v) === 0}
                    onClick={() => {
                      setPainelDoSom(null);
                      aoSepararAudio(painelDoSom.id);
                    }}
                    className="flex h-8 flex-1 items-center justify-center gap-1 rounded-[8px] bg-[#CCFBF1] text-[11.5px] font-bold text-[#115E59] transition hover:bg-[#99F6E4] disabled:opacity-40"
                    title="O som vira uma faixa própria na trilha de áudio e o vídeo fica mudo"
                  >
                    <Unlink2 size={13} /> Separar áudio
                  </button>
                </div>
              </div>
            );
          })()
        : null}
    </section>
  );
}
