import { useRef } from "react"; import { useEnableKeyframes, isPlayheadWithinTween, type EnableKeyframesSession, } from "../hooks/useEnableKeyframes"; import { computeElementPercentage } from "../hooks/gsapShared"; import { getNextTimelineZoomPercent, getTimelineZoomPercent, } from "../player/components/timelineZoom"; import { useTimelineZoom } from "../player/components/useTimelineZoom"; import { getTimelineToggleTitle } from "../utils/timelineDiscovery"; import { usePlayerStore, type TimelineElement } from "../player"; import { STUDIO_KEYFRAMES_ENABLED, STUDIO_RAZOR_TOOL_ENABLED, } from "./editor/manualEditingAvailability"; import { Tooltip } from "./ui"; import { Scissors } from "../icons/SystemIcons"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { DomEditSelection } from "./editor/domEditingTypes"; import { canSplitElement } from "../utils/timelineElementSplit"; import { canAddBeatAt, addBeatAtCompositionTime } from "../utils/beatEditActions"; interface DomEditSessionSlice extends EnableKeyframesSession { domEditSelection: DomEditSelection | null; selectedGsapAnimations: GsapAnimation[]; } interface TimelineToolbarProps { toggleTimelineVisibility: () => void; domEditSession?: DomEditSessionSlice; onSplitElement?: (element: TimelineElement, splitTime: number) => void; } function useKeyframeToggle(session?: DomEditSessionSlice) { const currentTime = usePlayerStore((s) => s.currentTime); const sessionRef = useRef(session); sessionRef.current = session; const onToggle = useEnableKeyframes( sessionRef as React.RefObject, ); if (!session) return { state: "none" as const, onToggle: undefined }; const sel = session.domEditSelection; const anims = session.selectedGsapAnimations; const kfAnim = anims.find((a) => a.keyframes); let state: "active" | "inactive" | "none" = "none"; // Outside the tween, clicking extends the animation to the playhead rather than // toggling a (clamped) edge keyframe — so the button stays an "add" affordance. let willExtend = false; if (kfAnim?.keyframes && sel) { if (!isPlayheadWithinTween(kfAnim, currentTime)) { state = "inactive"; willExtend = true; } else { // Tween-relative percentage (not the clip range) so the button state matches // where the keyframe would actually land. const pct = computeElementPercentage(currentTime, sel, kfAnim); state = kfAnim.keyframes.keyframes.some((k) => Math.abs(k.percentage - pct) <= 1) ? "active" : "inactive"; } } return { state, willExtend, onToggle: sel ? onToggle : undefined }; } // fallow-ignore-next-line complexity export function TimelineToolbar({ toggleTimelineVisibility, domEditSession, onSplitElement, }: TimelineToolbarProps) { const activeTool = usePlayerStore((s) => s.activeTool); const setActiveTool = usePlayerStore((s) => s.setActiveTool); // Subscribe so the add-beat button reacts to playhead movement and analysis load. const currentTime = usePlayerStore((s) => s.currentTime); const beatAnalysisReady = usePlayerStore((s) => s.beatAnalysis !== null); const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom(); const displayedTimelineZoomPercent = getTimelineZoomPercent(zoomMode, manualZoomPercent); const { state: keyframeState, willExtend: keyframeWillExtend, onToggle: onToggleKeyframe, } = useKeyframeToggle(domEditSession); return (
Timeline
{STUDIO_RAZOR_TOOL_ENABLED && (
)} {STUDIO_KEYFRAMES_ENABLED && onToggleKeyframe && ( <> )} {onSplitElement && (() => { const { selectedElementId, elements, currentTime } = usePlayerStore.getState(); const el = selectedElementId ? elements.find((e) => (e.key ?? e.id) === selectedElementId) : null; if (!el || !canSplitElement(el)) return null; const canSplit = currentTime > el.start && currentTime < el.start + el.duration; return ( ); })()} {beatAnalysisReady && canAddBeatAt(currentTime) && (() => ( ))()}
{`${displayedTimelineZoomPercent}%`}
); }