'use client'; import * as React from 'react'; import type { CSSProperties } from 'react'; import { AnimatePresence, motion } from 'motion/react'; import type { ToolcraftTimelineKeyframeEasing, ToolcraftTimelineKeyframeGroup, } from '../../state/types'; import { clampToolcraftTimelineTime, roundToolcraftTimelineKeyframeTime, toolcraftTimelineScrubStepSeconds, } from '../../state/timeline-values'; import { getTimelineEventTargetElement, isEditableTimelineEventTarget, isTimelineInteractiveElement, } from './timeline-event-targets'; import { findTimelineKeyframe } from './timeline-keyframes'; import { TimelineKeyframeRow } from './timeline-keyframe-row'; import { getTimelineCalcPositionStyle, timelineExpandedTrackEndOffsetPx, timelineExpandedTrackStartOffsetPx, timelineKeyframePresenceTransition, timelineRulerLeftInsetPx, timelineRulerRightInsetPx, timelineTrackColumnBorderWidthPx, } from './timeline-panel-layout'; const timelinePlayheadSafeZonePx = 7; const timelinePlayheadHitAreaWidthPx = timelineTrackColumnBorderWidthPx + timelinePlayheadSafeZonePx * 2; type TimelineExpandedContentProps = { currentTimeSeconds: number; durationSeconds: number; isScrubbing: boolean; keyframeGroups: readonly ToolcraftTimelineKeyframeGroup[]; onChangeKeyframeEasing: (keyframeId: string, easing: ToolcraftTimelineKeyframeEasing) => void; onDeleteControlKeyframes: (controlId: string) => void; onDeleteKeyframe: (keyframeId: string) => void; onKeyframeDragStart: () => void; onKeyDown: (event: React.KeyboardEvent) => void; onLostPointerCapture: () => void; onMoveKeyframe: (keyframeId: string, timeSeconds: number) => string | null; onPointerDown: (event: React.PointerEvent) => void; onPointerMove: (event: React.PointerEvent) => void; onPointerUp: (event: React.PointerEvent) => void; onSelectedKeyframeChange: (keyframeId: string | null) => void; selectedKeyframeId: string | null; stripRef: React.RefObject; }; function cn(...classNames: Array): string { return classNames.filter(Boolean).join(' '); } function formatTimelineSeconds(value: number): string { return value.toFixed(2); } function getTimelineRulerTicks(durationSeconds: number): number[] { return [0, 0.25, 0.5, 0.75, 1].map((ratio) => durationSeconds * ratio); } function getTimelineRulerMarkRatios(): number[] { return Array.from({ length: 33 }, (_value, index) => index / 32); } function getTimelineTrackPositionStyle( currentTimeSeconds: number, durationSeconds: number, ): CSSProperties { const ratio = Math.max(0, Math.min(1, currentTimeSeconds / durationSeconds)); return getTimelineCalcPositionStyle( ratio, timelineExpandedTrackStartOffsetPx * (1 - ratio) - timelineExpandedTrackEndOffsetPx * ratio, ); } export function TimelineExpandedContent({ currentTimeSeconds, durationSeconds, isScrubbing, keyframeGroups, onChangeKeyframeEasing, onDeleteControlKeyframes, onDeleteKeyframe, onKeyframeDragStart, onKeyDown, onLostPointerCapture, onMoveKeyframe, onPointerDown, onPointerMove, onPointerUp, onSelectedKeyframeChange, selectedKeyframeId, stripRef, }: TimelineExpandedContentProps): React.JSX.Element { const trackPlayheadStyle = getTimelineTrackPositionStyle(currentTimeSeconds, durationSeconds); const selectedKeyframe = findTimelineKeyframe(keyframeGroups, selectedKeyframeId); const deleteSelectedKeyframe = (): void => { if (!selectedKeyframeId) { return; } onDeleteKeyframe(selectedKeyframeId); onSelectedKeyframeChange(null); }; const moveSelectedKeyframeByStep = (direction: -1 | 1): void => { if (!selectedKeyframe) { return; } const nextTimeSeconds = roundToolcraftTimelineKeyframeTime( clampToolcraftTimelineTime( selectedKeyframe.timeSeconds + toolcraftTimelineScrubStepSeconds * direction, durationSeconds, ), ); if (nextTimeSeconds === selectedKeyframe.timeSeconds) { return; } const nextSelectedKeyframeId = onMoveKeyframe(selectedKeyframe.id, nextTimeSeconds); onSelectedKeyframeChange(nextSelectedKeyframeId ?? selectedKeyframe.id); }; const handleExpandedKeyDown = (event: React.KeyboardEvent): void => { if (isEditableTimelineEventTarget(event.target)) { return; } if (event.key === 'Delete' || event.key === 'Backspace') { if (!selectedKeyframeId) { return; } event.preventDefault(); deleteSelectedKeyframe(); return; } if (event.key === 'Escape' && selectedKeyframeId) { event.preventDefault(); onSelectedKeyframeChange(null); return; } if (event.key === 'ArrowLeft' && selectedKeyframe) { event.preventDefault(); moveSelectedKeyframeByStep(-1); return; } if (event.key === 'ArrowRight' && selectedKeyframe) { event.preventDefault(); moveSelectedKeyframeByStep(1); return; } onKeyDown(event); }; const handleExpandedPointerDown = (event: React.PointerEvent): void => { const targetElement = getTimelineEventTargetElement(event.target); const clickedKeyframe = targetElement?.closest('[data-slot="timeline-keyframe"]'); const clickedInteractiveElement = isTimelineInteractiveElement(event.target); if (!clickedKeyframe && !clickedInteractiveElement && selectedKeyframeId) { onSelectedKeyframeChange(null); } if (clickedInteractiveElement && !clickedKeyframe) { return; } onPointerDown(event); }; return (
Properties
{getTimelineRulerTicks(durationSeconds).map((tick, index, ticks) => ( {Math.round(tick)} ))}
); }