import { useState, useCallback, useMemo, useRef } from "react"; import { useMountEffect } from "../../hooks/useMountEffect"; import { usePlayerStore } from "../store/playerStore"; import { formatTime } from "../lib/time"; interface EditPopoverProps { rangeStart: number; rangeEnd: number; anchorX: number; anchorY: number; onClose: () => void; } export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }: EditPopoverProps) { const elements = usePlayerStore((s) => s.elements); const [prompt, setPrompt] = useState(""); const [copied, setCopied] = useState(false); const popoverRef = useRef(null); const textareaRef = useRef(null); const start = Math.min(rangeStart, rangeEnd); const end = Math.max(rangeStart, rangeEnd); const elementsInRange = useMemo(() => { return elements.filter((el) => { const elEnd = el.start + el.duration; return el.start < end && elEnd > start; }); }, [elements, start, end]); useMountEffect(() => { setTimeout(() => textareaRef.current?.focus(), 50); }); useMountEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }); useMountEffect(() => { const handleClick = (e: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { onClose(); } }; setTimeout(() => window.addEventListener("mousedown", handleClick), 100); return () => window.removeEventListener("mousedown", handleClick); }); const buildClipboardText = useCallback(() => { const elementLines = elementsInRange .map( (el) => `- #${el.id} (${el.tag}) — ${formatTime(el.start)} to ${formatTime(el.start + el.duration)}, track ${el.track}`, ) .join("\n"); return `Edit the following HyperFrames composition: Time range: ${formatTime(start)} — ${formatTime(end)} Elements in range: ${elementLines || "(none)"} User request: ${prompt.trim() || "(no prompt provided)"} Instructions: Modify only the elements listed above within the specified time range. The composition uses HyperFrames data attributes (data-start, data-duration, data-track-index) and GSAP for animations. Preserve all other elements and timing outside this range.`; }, [start, end, elementsInRange, prompt]); const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(buildClipboardText()); } catch { const ta = document.createElement("textarea"); ta.value = buildClipboardText(); document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } setCopied(true); setTimeout(() => { setCopied(false); onClose(); }, 800); }, [buildClipboardText, onClose]); const style: React.CSSProperties = { position: "fixed", left: Math.max(8, Math.min(anchorX - 160, window.innerWidth - 336)), top: Math.max(8, anchorY - 280), zIndex: 200, }; return (
{/* Header */}
{formatTime(start)} — {formatTime(end)}
{elementsInRange.length} element{elementsInRange.length !== 1 ? "s" : ""}
{/* Elements */} {elementsInRange.length > 0 && (
{elementsInRange.map((el) => (
#{el.id} {el.tag}
))}
)} {/* Prompt */}