import { resolveTimelineMinimapPreview, type TimelineMinimapItem } from "./timelineMinimapItems"; import { useRef, useState, type MouseEvent } from "react"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { Button } from "../ui/button"; import { resolveTimelineMinimapHeightStyle, resolveTimelineMinimapIndexFromPointer, resolveTimelineMinimapNavigationInteractive, resolveTimelineMinimapTopPercent, TIMELINE_MINIMAP_MIN_ITEMS, } from "./MessagesTimeline.logic"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; // A release outside the window can skip `mouseup`, so the next button-free // `mousemove` also counts. function onMouseButtonsReleased(onRelease: () => void): () => void { const check = (event: globalThis.MouseEvent) => { if (event.buttons !== 0) return; stop(); onRelease(); }; const stop = () => { window.removeEventListener("mouseup", check); window.removeEventListener("mousemove", check); }; window.addEventListener("mouseup", check); window.addEventListener("mousemove", check); return stop; } export function TimelineMinimap({ hasPersistentGutter, hitStripWidth, currentIndex, items, stripMap, onWheel, onSelect, }: { hasPersistentGutter: boolean; hitStripWidth: number; currentIndex: number | null; items: ReadonlyArray; stripMap: Map; onWheel: (event: WheelEvent) => void; onSelect: (item: TimelineMinimapItem) => void; }) { const [activeIndex, setActiveIndex] = useState(null); const wheelTargetRef = (node: HTMLDivElement | null) => { if (!node) return; node.addEventListener("wheel", onWheel, { passive: false }); return () => node.removeEventListener("wheel", onWheel); }; const pressedOnStripRef = useRef(false); const [passThrough, setPassThrough] = useState(false); const resolvedActiveIndex = activeIndex !== null && activeIndex < items.length ? activeIndex : null; const activeItem = resolveTimelineMinimapPreview( resolvedActiveIndex === null ? null : (items[resolvedActiveIndex] ?? null), ); const navigationInteractive = resolveTimelineMinimapNavigationInteractive(hitStripWidth) && !passThrough; const activeTopPercent = resolvedActiveIndex === null ? 0 : resolveTimelineMinimapTopPercent(resolvedActiveIndex, items.length); const resolvedCurrentIndex = currentIndex !== null && currentIndex >= 0 && currentIndex < items.length ? currentIndex : null; const previousItem = resolvedCurrentIndex === null ? null : (items[resolvedCurrentIndex - 1] ?? null); const nextItem = resolvedCurrentIndex === null ? null : (items[resolvedCurrentIndex + 1] ?? null); const resolveActiveIndexFromPointer = (event: MouseEvent) => { const rect = event.currentTarget.getBoundingClientRect(); return resolveTimelineMinimapIndexFromPointer({ itemCount: items.length, railTop: rect.top, railHeight: rect.height, pointerY: event.clientY, }); }; const updateActiveIndexFromPointer = (event: MouseEvent) => { const nextIndex = resolveActiveIndexFromPointer(event); setActiveIndex(nextIndex); }; const moveActiveIndex = (delta: number) => { setActiveIndex((current) => { const base = current ?? 0; return Math.max(0, Math.min(items.length - 1, base + delta)); }); }; if (items.length < TIMELINE_MINIMAP_MIN_ITEMS) { return null; } return (
0 && !passThrough ? "pointer-events-auto" : "pointer-events-none", )} onMouseDown={() => { if (pressedOnStripRef.current) return; pressedOnStripRef.current = true; onMouseButtonsReleased(() => { pressedOnStripRef.current = false; }); }} onMouseMove={(event) => { if (event.buttons === 0 || pressedOnStripRef.current || passThrough) return; setPassThrough(true); onMouseButtonsReleased(() => setPassThrough(false)); }} style={{ height: resolveTimelineMinimapHeightStyle(items.length), width: hitStripWidth, }} > { if (previousItem) onSelect(previousItem); }} /> { if (nextItem) onSelect(nextItem); }} />
); } function TimelineMinimapNavigationButton({ direction, disabled, interactive, onClick, }: { direction: "previous" | "next"; disabled: boolean; interactive: boolean; onClick: () => void; }) { const previous = direction === "previous"; const label = previous ? "Previous turn" : "Next turn"; const Icon = previous ? ChevronUpIcon : ChevronDownIcon; return ( } > {label} ); }