import { useCallback } from "react";
import type { StudioRightPanelProps } from "./StudioRightPanel.types";
export type { StudioRightPanelProps };
import { PropertyPanel } from "./editor/PropertyPanel";
import { LayersPanel } from "./editor/LayersPanel";
import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
import { BlockParamsPanel } from "./editor/BlockParamsPanel";
import { RenderQueuePanel } from "./renders/RenderQueuePanel";
import { SlideshowPanel } from "./panels/SlideshowPanel";
import { VariablesPanel } from "./panels/VariablesPanel";
import { PanelTabButton } from "./PanelTabButton";
import type { RenderJob } from "./renders/useRenderQueue";
import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
import { useFileManagerContext } from "../contexts/FileManagerContext";
import { useDomEditContext } from "../contexts/DomEditContext";
import { usePlayerStore } from "../player";
import {
applyColorGradingScopeUpdate,
EMPTY_COLOR_GRADING_SCOPE_RESULT,
type ColorGradingScope,
} from "./studioColorGradingScope";
import { timelineKeysForSelections } from "../utils/studioHelpers";
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
import { useRemoveBackground } from "../hooks/useRemoveBackground";
// fallow-ignore-next-line complexity
export function StudioRightPanel({
designPanelActive,
activeBlockParams,
onCloseBlockParams,
recordingState,
recordingDuration,
onToggleRecording,
sdkSession,
publishSdkSession,
forceReloadSdkSession,
reloadPreview,
domEditSaveTimestampRef,
recordEdit,
onToggleElementHidden,
onAutoGroupCarveSources,
onAddMediaOverlay,
}: StudioRightPanelProps) {
const {
rightWidth,
adjustPanelWidth,
rightPanelTab,
setRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
setExclusiveRightInspectorPane,
handlePanelResizeStart,
handlePanelResizeMove,
handlePanelResizeEnd,
} = usePanelLayoutContext();
const {
previewIframeRef,
projectId,
activeCompPath,
showToast,
waitForPendingDomEditSaves,
renderQueue,
} = useStudioShellContext();
const { captionEditMode, refreshKey } = useStudioPlaybackContext();
const {
domEditSelection,
domEditGroupSelections,
copiedAgentPrompt,
clearDomSelection,
handleUngroupSelection,
handleGroupSelection,
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomAttributeQuietCommit,
handleDomHtmlAttributeCommit,
handleDomAttributesCommit,
handleDomPathOffsetCommit,
handleDomBoxSizeCommit,
handleDomRotationCommit,
handleDomTextCommit,
handleDomTextFieldStyleCommit,
handleDomAddTextField,
handleDomRemoveTextField,
handleAskAgent,
selectedGsapAnimations,
gsapMultipleTimelines,
gsapUnsupportedTimelinePattern,
handleGsapUpdateProperty,
handleGsapUpdateMeta,
handleGsapDeleteAnimation,
handleGsapAddAnimation,
handleGsapAddProperty,
handleGsapRemoveProperty,
handleGsapUpdateFromProperty,
handleGsapAddFromProperty,
handleGsapRemoveFromProperty,
commitAnimatedProperty,
commitAnimatedProperties,
handleSetArcPath,
handleUpdateArcSegment,
handleUnroll,
handleUpdateKeyframeEase,
handleUpdateSegmentEase,
handleSetAllKeyframeEases,
handleGsapAddKeyframe,
handleGsapRemoveKeyframe,
handleGsapConvertToKeyframes,
} = useDomEditContext();
const {
assets,
fontAssets,
projectDir,
handleImportFiles,
handleImportFonts,
refreshFileTree,
readProjectFile,
writeProjectFile,
fileTree,
editingFile,
} = useFileManagerContext();
// Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately,
// no coalescing — each is a distinct user action that deserves its own undo entry.
const onPersistSlideshow = useSlideshowPersist({
sdkSession,
activeCompPath,
readProjectFile,
writeProjectFile,
recordEdit,
reloadPreview,
domEditSaveTimestampRef,
publishSdkSession,
});
// Notes path: persists are debounced in SlideshowPanel; coalesceKey ensures
// rapid writes collapse into a single undo entry via the save-queue infra.
const onPersistSlideshowNotes = useSlideshowPersist({
sdkSession,
activeCompPath,
readProjectFile,
writeProjectFile,
recordEdit,
reloadPreview,
domEditSaveTimestampRef,
publishSdkSession,
coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
});
const {
layersPanePercent,
splitContainerRef,
handleInspectorSplitResizeStart,
handleInspectorSplitResizeMove,
handleInspectorSplitResizeEnd,
} = useInspectorSplitResize();
const renderJobs = renderQueue.jobs as RenderJob[];
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
editingFileContent: editingFile?.content,
previewIframeRef,
refreshKey,
rightPanelTab,
setRightPanelTab,
});
const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers;
const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
if (!inspectorTabActive) {
setRightPanelTab(pane);
return;
}
// Flat inspector: Layers always renders full-height by itself (see the
// render branch below), so the two panes are mutually exclusive here —
// otherwise both tabs could show "active" while only one actually shows.
if (STUDIO_FLAT_INSPECTOR_ENABLED) {
setExclusiveRightInspectorPane(pane);
return;
}
toggleRightInspectorPane(pane);
};
const handleApplyColorGradingScope = useCallback(
async (scope: ColorGradingScope, value: string | null) =>
applyColorGradingScopeUpdate({
scope,
value,
selectedSourceFile: domEditSelection?.sourceFile || activeCompPath || "index.html",
fileTree,
projectId,
domEditSaveTimestampRef,
waitForPendingDomEditSaves,
readProjectFile,
writeProjectFile,
recordEdit,
reloadPreview,
showToast,
}).catch((error) => {
showToast(
`Couldn't apply color grading: ${error instanceof Error ? error.message : String(error)}`,
"error",
);
return EMPTY_COLOR_GRADING_SCOPE_RESULT;
}),
[
activeCompPath,
domEditSaveTimestampRef,
domEditSelection?.sourceFile,
fileTree,
projectId,
readProjectFile,
recordEdit,
reloadPreview,
showToast,
waitForPendingDomEditSaves,
writeProjectFile,
],
);
const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast);
/**
* A dial being dragged writes to the preview and stops there.
*
* Every one of these panels previews on each pointermove and commits on
* release. Persisting the moves too put a fragment of the drag in the undo
* stack — and since those writes race, history could not coalesce them
* reliably, so undo took back a sliver of the gesture rather than the gesture.
* The release's own commit is what reaches the file and the undo stack.
*/
const setAttributeWhileDragging = useCallback(
(attr: string, value: string | null) =>
handleDomAttributeLiveCommit(attr, value, undefined, { previewOnly: true }),
[handleDomAttributeLiveCommit],
);
const handleHideAllSelected = () => {
const { elements } = usePlayerStore.getState();
const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
if (keys.length > 0) void onToggleElementHidden?.(keys, true);
};
const propertyPanel = (
Inspector is unavailable right now — select the Design or Layers pane above, or pause playback/recording to inspect elements.