import { memo, useCallback } from "react"; import { useCaptionStore } from "../store"; import type { CaptionAnimation } from "../types"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const ENTRANCE_PRESETS = [ "none", "fade", "slide-up", "slide-down", "slide-left", "slide-right", "pop", "slam", "bounce", "typewriter", "blur-in", "flip", "drop", ]; const HIGHLIGHT_PRESETS = [ "none", "color-change", "scale-pop", "glow-pulse", "underline-sweep", "background-fill", "bounce", ]; const EXIT_PRESETS = [ "none", "fade", "slide-up", "slide-down", "slide-left", "slide-right", "scatter", "drop", "collapse", "blur-out", "shrink", ]; const EASE_PRESETS = [ "power1.out", "power2.out", "power3.out", "power4.out", "power1.in", "power2.in", "power3.in", "power1.inOut", "power2.inOut", "back.out(1.7)", "elastic.out(1,0.3)", "bounce.out", ]; import { Section, Row, inputCls } from "./shared"; // --------------------------------------------------------------------------- // Animation phase controls // --------------------------------------------------------------------------- interface AnimationPhaseProps { label: string; presets: string[]; animation: CaptionAnimation | null; showIntensity?: boolean; onChange: (update: Partial) => void; } function AnimationPhase({ label, presets, animation, showIntensity, onChange, }: AnimationPhaseProps) { const preset = animation?.preset ?? "none"; const duration = animation?.duration ?? 0.2; const ease = animation?.ease ?? "power2.out"; const stagger = animation?.stagger ?? 0; const intensity = animation?.intensity ?? 1; return (
onChange({ duration: Number(e.target.value) })} className={inputCls} /> onChange({ stagger: Number(e.target.value) })} className={inputCls} /> {showIntensity && (
onChange({ intensity: Number(e.target.value) })} className="flex-1 accent-studio-accent" /> {intensity.toFixed(2)}
)}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export const CaptionAnimationPanel = memo(function CaptionAnimationPanel() { const model = useCaptionStore((s) => s.model); const selectedGroupId = useCaptionStore((s) => s.selectedGroupId); const selectedSegmentIds = useCaptionStore((s) => s.selectedSegmentIds); const updateGroupAnimation = useCaptionStore((s) => s.updateGroupAnimation); const applyAnimationToAll = useCaptionStore((s) => s.applyAnimationToAll); // Resolve which group to edit let resolvedGroupId: string | null = selectedGroupId; if (!resolvedGroupId && model && selectedSegmentIds.size > 0) { const firstSegmentId = [...selectedSegmentIds][0]; if (firstSegmentId) { for (const [gid, group] of model.groups) { if (group.segmentIds.includes(firstSegmentId)) { resolvedGroupId = gid; break; } } } } const group = resolvedGroupId ? model?.groups.get(resolvedGroupId) : undefined; const animation = group?.animation; // All hooks must be called before any early return const handleEntranceChange = useCallback( (update: Partial) => { if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "entrance", update); }, [resolvedGroupId, updateGroupAnimation], ); const handleHighlightChange = useCallback( (update: Partial) => { if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "highlight", update); }, [resolvedGroupId, updateGroupAnimation], ); const handleExitChange = useCallback( (update: Partial) => { if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "exit", update); }, [resolvedGroupId, updateGroupAnimation], ); const handleApplyToAll = useCallback(() => { if (animation) applyAnimationToAll(animation); }, [animation, applyAnimationToAll]); // Empty state — after all hooks if (!group || !resolvedGroupId || !animation) { return (

Select a caption group to edit animations

); } return (
{/* Scrollable content */}
{/* Footer */}
); });