"use client"; import Link from "next/link"; import { type ChangeEvent, type CSSProperties, type KeyboardEvent, type MouseEvent, type PointerEvent, useCallback, useEffect, useId, useRef, useState, } from "react"; import { useReducedMotion } from "@/app/hooks/useReducedMotion"; import { isMobileViewport, isSaveData } from "@/lib/device"; import { chapterFraction, chapterIndexAt, type Device, desktopFractionAt, getShowcaseAssets, mobileFractionAt, } from "@/lib/project-showcase"; import { BrowserFrame, displayUrl } from "./BrowserFrame"; import { PhoneFrame } from "./PhoneFrame"; import type { ShowcaseItem } from "./ShowcaseProvider"; import "./lightbox.css"; const formatTime = (seconds: number) => { const whole = Math.max(0, Math.floor(seconds)); return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`; }; /** * Vídeo ampliado: janela modal nativa (: foco preso, Esc fecha, foco volta ao botão que abriu), * com troca desktop/celular que mantém o ponto do site, capítulos clicáveis e controles próprios. */ export function ProjectLightbox({ items, index, onClose, onNavigate, }: { items: ShowcaseItem[]; index: number; onClose: () => void; onNavigate: (index: number) => void; }) { const { config, project } = items[index]; const reducedMotion = useReducedMotion(); const titleId = useId(); const dialogRef = useRef(null); const videoRef = useRef(null); const closeRef = useRef(null); const previousRef = useRef(null); const nextRef = useRef(null); const pendingFraction = useRef(null); const pressedOnBackdrop = useRef(false); // celular abaixo de 768 px (lib/device.ts); a troca de aparelho continua à escolha do visitante const [device, setDevice] = useState(() => isMobileViewport() ? "mobile" : "desktop", ); // o vídeo de cada aparelho/projeto nasce tocando só se o visitante não pausou e não pediu menos movimento // "economizar dados": o arquivo só chega quando o visitante pede (play), não antes const [saveData] = useState(isSaveData); const [autoplay, setAutoplay] = useState( () => !window.matchMedia("(prefers-reduced-motion: reduce)").matches && !saveData, ); const wantsToPlay = useRef(autoplay); const [playing, setPlaying] = useState(false); const [fraction, setFraction] = useState(0); const [duration, setDuration] = useState(0); // lê a duração e aplica o ponto pendente da troca de aparelho; vale para o `loadedmetadata` e para o vídeo que já // chegou com os metadados (cache, preload): nesse caso o evento passou antes de alguém ouvir e não volta const readMetadata = useCallback(() => { const video = videoRef.current; if (!video) return; setDuration(video.duration); if (pendingFraction.current !== null) { video.currentTime = pendingFraction.current * video.duration; pendingFraction.current = null; } }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: o