import { memo, useCallback, useEffect, useRef, useState } from "react"; import { MagnetStraight, GridFour, Path } from "@phosphor-icons/react"; import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences"; import { usePlayerStore } from "../../player/store/playerStore"; const SNAP_DEFAULTS = { snapEnabled: true, gridVisible: false, gridSpacing: 50, snapToGrid: false, }; // fallow-ignore-next-line complexity function readSnapPrefs() { const prefs = readStudioUiPreferences(); return { snapEnabled: prefs.snapEnabled ?? SNAP_DEFAULTS.snapEnabled, gridVisible: prefs.gridVisible ?? SNAP_DEFAULTS.gridVisible, gridSpacing: prefs.gridSpacing ?? SNAP_DEFAULTS.gridSpacing, snapToGrid: prefs.snapToGrid ?? SNAP_DEFAULTS.snapToGrid, }; } interface SnapToolbarProps { onSnapChange?: (prefs: { snapEnabled: boolean; gridVisible: boolean; gridSpacing: number; snapToGrid: boolean; }) => void; } // fallow-ignore-next-line complexity export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolbarProps) { const [prefs, setPrefs] = useState(readSnapPrefs); const [gridPopoverOpen, setGridPopoverOpen] = useState(false); // Motion-path "set destination" toggle — shown only when the selected element // can take a path; arms a single canvas click to place it (MotionPathOverlay). const motionPathCreateAvailable = usePlayerStore((s) => s.motionPathCreateAvailable); const motionPathArmed = usePlayerStore((s) => s.motionPathArmed); const setMotionPathArmed = usePlayerStore((s) => s.setMotionPathArmed); const popoverRef = useRef(null); const gridButtonRef = useRef(null); const updatePrefs = useCallback( (patch: Partial) => { setPrefs((prev) => { const next = { ...prev, ...patch }; writeStudioUiPreferences(patch); onSnapChange?.(next); return next; }); }, [onSnapChange], ); const toggleSnap = useCallback(() => { updatePrefs({ snapEnabled: !prefs.snapEnabled }); }, [prefs.snapEnabled, updatePrefs]); const toggleGrid = useCallback(() => { updatePrefs({ gridVisible: !prefs.gridVisible }); }, [prefs.gridVisible, updatePrefs]); useEffect(() => { // fallow-ignore-next-line complexity const handleKeyDown = (e: KeyboardEvent) => { const t = e.target; if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) return; if (t instanceof HTMLElement && t.isContentEditable) return; if (t instanceof HTMLIFrameElement) return; if (e.key === "s" && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); updatePrefs({ snapEnabled: !readSnapPrefs().snapEnabled }); } if (e.key === "g" && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); updatePrefs({ gridVisible: !readSnapPrefs().gridVisible }); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [updatePrefs]); useEffect(() => { if (!gridPopoverOpen) return; const handleClickOutside = (e: MouseEvent) => { const target = e.target as Node; if (popoverRef.current?.contains(target) || gridButtonRef.current?.contains(target)) return; setGridPopoverOpen(false); }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [gridPopoverOpen]); return (
e.stopPropagation()} > {motionPathCreateAvailable && ( )}
{gridPopoverOpen && (
)}
); });