"use client" import * as React from "react" import { motion, AnimatePresence } from "framer-motion" import { ChevronLeft, ChevronRight, X, ExternalLink, ArrowLeft } from "lucide-react" import { tileUrl } from "@/lib/api" import type { Hit } from "@/lib/types" import { Button } from "@/components/ui/button" interface LightboxProps { hit: Hit allHits: Hit[] onClose: () => void onNavigate: (hit: Hit) => void } export function Lightbox({ hit, allHits, onClose, onNavigate }: LightboxProps) { const currentIndex = allHits.findIndex((h) => h.vector_id === hit.vector_id) const hasPrev = currentIndex > 0 const hasNext = currentIndex < allHits.length - 1 // Pan and zoom state const [scale, setScale] = React.useState(1) const [translate, setTranslate] = React.useState({ x: 0, y: 0 }) const [isDragging, setIsDragging] = React.useState(false) const dragStart = React.useRef({ x: 0, y: 0 }) const translateStart = React.useRef({ x: 0, y: 0 }) // Derive article title and full URL const raw = hit.url const slug = raw.includes("/wiki/") ? raw.split("/wiki/").pop()! : raw const title = decodeURIComponent(slug).replace(/_/g, " ") || `Article #${hit.article_id}` const articleUrl = raw.startsWith("http") ? raw : `https://en.wikipedia.org/wiki/${encodeURIComponent(slug)}` // Lock body scroll while lightbox is open React.useEffect(() => { const prev = document.body.style.overflow document.body.style.overflow = "hidden" return () => { document.body.style.overflow = prev } }, []) // Reset pan/zoom when navigating to a different tile — use key prop on parent instead // (handled by passing key={hit.vector_id} to Lightbox in page.tsx) // Keyboard navigation React.useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === "Escape") onClose() if (e.key === "ArrowLeft" && hasPrev) onNavigate(allHits[currentIndex - 1]) if (e.key === "ArrowRight" && hasNext) onNavigate(allHits[currentIndex + 1]) } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, [onClose, onNavigate, allHits, currentIndex, hasPrev, hasNext]) // Zoom via mouse wheel (native event to allow preventDefault on non-passive listener) const imageAreaRef = React.useRef(null) React.useEffect(() => { const el = imageAreaRef.current if (!el) return function onWheel(e: WheelEvent) { e.preventDefault() e.stopPropagation() setScale((prev) => Math.max(0.5, Math.min(5, prev - e.deltaY * 0.001))) } el.addEventListener("wheel", onWheel, { passive: false }) return () => el.removeEventListener("wheel", onWheel) }, []) // Pan via drag function handleMouseDown(e: React.MouseEvent) { if (e.button !== 0) return setIsDragging(true) dragStart.current = { x: e.clientX, y: e.clientY } translateStart.current = { ...translate } } function handleMouseMove(e: React.MouseEvent) { if (!isDragging) return setTranslate({ x: translateStart.current.x + (e.clientX - dragStart.current.x), y: translateStart.current.y + (e.clientY - dragStart.current.y), }) } function handleMouseUp() { setIsDragging(false) } return ( {/* Dark backdrop */}
{/* Content container */} e.stopPropagation()} > {/* Image area */}
{/* Back button bar — prominent, always visible */}
{/* eslint-disable-next-line @next/next/no-img-element */} {`Tile
{/* Metadata sidebar */}
{/* Close button */}

{title}

Open article
{/* Navigation buttons */}
) } function MetaRow({ label, value, accent, }: { label: string value: string accent?: boolean }) { return (
{label} {value}
) }