@keplar-404/react-timeline-editor 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/components/control_area/index.d.ts +0 -0
  2. package/dist/components/cursor/cursor.d.ts +20 -0
  3. package/dist/components/cut-overlay/CutOverlay.d.ts +202 -0
  4. package/dist/components/edit_area/cross_row_drag.d.ts +50 -0
  5. package/dist/components/edit_area/drag_lines.d.ts +11 -0
  6. package/dist/components/edit_area/drag_preview.d.ts +14 -0
  7. package/dist/components/edit_area/drag_utils.d.ts +39 -0
  8. package/dist/components/edit_area/edit_action.d.ts +19 -0
  9. package/dist/components/edit_area/edit_area.d.ts +56 -0
  10. package/dist/components/edit_area/edit_row.d.ts +27 -0
  11. package/dist/components/edit_area/hooks/use_drag_line.d.ts +33 -0
  12. package/dist/components/edit_area/insertion_line.d.ts +12 -0
  13. package/dist/components/loop-zone/LoopZoneOverlay.d.ts +243 -0
  14. package/dist/components/row_rnd/hooks/useAutoScroll.d.ts +7 -0
  15. package/dist/components/row_rnd/interactable.d.ts +14 -0
  16. package/dist/components/row_rnd/row_rnd.d.ts +3 -0
  17. package/dist/components/row_rnd/row_rnd_interface.d.ts +47 -0
  18. package/dist/components/time_area/time_area.d.ts +17 -0
  19. package/dist/components/timeline.d.ts +3 -0
  20. package/dist/components/transport/TransportBar.d.ts +132 -0
  21. package/dist/components/transport/useTimelinePlayer.d.ts +164 -0
  22. package/dist/index.cjs.js +13 -0
  23. package/dist/index.d.ts +12 -0
  24. package/dist/index.es.js +10102 -0
  25. package/dist/index.umd.js +13 -0
  26. package/dist/interface/common_prop.d.ts +12 -0
  27. package/dist/interface/const.d.ts +28 -0
  28. package/dist/interface/timeline.d.ts +342 -0
  29. package/dist/react-timeline-editor.css +1 -0
  30. package/dist/utils/check_props.d.ts +2 -0
  31. package/dist/utils/deal_class_prefix.d.ts +1 -0
  32. package/dist/utils/deal_data.d.ts +58 -0
  33. package/dist/utils/logger.d.ts +132 -0
  34. package/package.json +70 -0
  35. package/src/components/control_area/index.tsx +1 -0
  36. package/src/components/cursor/cursor.css +26 -0
  37. package/src/components/cursor/cursor.tsx +105 -0
  38. package/src/components/cut-overlay/CutOverlay.css +68 -0
  39. package/src/components/cut-overlay/CutOverlay.tsx +491 -0
  40. package/src/components/edit_area/cross_row_drag.tsx +174 -0
  41. package/src/components/edit_area/drag_lines.css +13 -0
  42. package/src/components/edit_area/drag_lines.tsx +31 -0
  43. package/src/components/edit_area/drag_preview.tsx +50 -0
  44. package/src/components/edit_area/drag_utils.ts +77 -0
  45. package/src/components/edit_area/edit_action.css +56 -0
  46. package/src/components/edit_area/edit_action.tsx +362 -0
  47. package/src/components/edit_area/edit_area.css +24 -0
  48. package/src/components/edit_area/edit_area.tsx +606 -0
  49. package/src/components/edit_area/edit_row.css +78 -0
  50. package/src/components/edit_area/edit_row.tsx +128 -0
  51. package/src/components/edit_area/hooks/use_drag_line.ts +93 -0
  52. package/src/components/edit_area/insertion_line.tsx +39 -0
  53. package/src/components/loop-zone/LoopZoneOverlay.css +65 -0
  54. package/src/components/loop-zone/LoopZoneOverlay.tsx +461 -0
  55. package/src/components/row_rnd/hooks/useAutoScroll.ts +81 -0
  56. package/src/components/row_rnd/interactable.tsx +55 -0
  57. package/src/components/row_rnd/row_rnd.tsx +365 -0
  58. package/src/components/row_rnd/row_rnd_interface.ts +59 -0
  59. package/src/components/time_area/time_area.css +35 -0
  60. package/src/components/time_area/time_area.tsx +93 -0
  61. package/src/components/timeline.css +12 -0
  62. package/src/components/timeline.tsx +227 -0
  63. package/src/components/transport/TransportBar.css +171 -0
  64. package/src/components/transport/TransportBar.tsx +322 -0
  65. package/src/components/transport/useTimelinePlayer.ts +319 -0
  66. package/src/index.tsx +17 -0
  67. package/src/interface/common_prop.ts +13 -0
  68. package/src/interface/const.ts +32 -0
  69. package/src/interface/timeline.ts +329 -0
  70. package/src/utils/check_props.ts +77 -0
  71. package/src/utils/deal_class_prefix.ts +6 -0
  72. package/src/utils/deal_data.ts +159 -0
  73. package/src/utils/logger.ts +239 -0
@@ -0,0 +1,606 @@
1
+ import React, {
2
+ useEffect,
3
+ useImperativeHandle,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ useCallback,
8
+ } from 'react';
9
+ import { AutoSizer, Grid, GridCellRenderer, OnScrollParams, ScrollParams } from 'react-virtualized';
10
+ import { TimelineAction, TimelineRow } from '@keplar-404/timeline-engine';
11
+ import { CommonProp } from '../../interface/common_prop';
12
+ import { EditData } from '../../interface/timeline';
13
+ import { prefix } from '../../utils/deal_class_prefix';
14
+ import { parserPixelToTime, parserTimeToPixel } from '../../utils/deal_data';
15
+ import { DragLines } from './drag_lines';
16
+ import './edit_area.css';
17
+ import { EditRow } from './edit_row';
18
+ import { useDragLine } from './hooks/use_drag_line';
19
+ import { calculateTotalHeight, getRowHeights, isValidDragTarget } from './drag_utils';
20
+ import { InsertionLine } from './insertion_line';
21
+ import { DragPreview } from './drag_preview';
22
+ import {
23
+ CrossRowDragProvider,
24
+ CrossRowGhost,
25
+ CrossRowDragState,
26
+ useCrossRowDrag,
27
+ } from './cross_row_drag';
28
+
29
+ export type EditAreaProps = CommonProp & {
30
+ /** Horizontal scroll offset */
31
+ scrollLeft: number;
32
+ /** Vertical scroll offset */
33
+ scrollTop: number;
34
+ /** Scroll callback for sync */
35
+ onScroll: (params: OnScrollParams) => void;
36
+ /** Update the editor data */
37
+ setEditorData: (params: TimelineRow[]) => void;
38
+ /** Horizontal auto-scroll delta */
39
+ deltaScrollLeft: (scrollLeft: number) => void;
40
+ /** Enable cross-row block drag */
41
+ enableCrossRowDrag?: boolean;
42
+ /** Show ghost preview while block dragging across rows */
43
+ enableGhostPreview?: boolean;
44
+ /**
45
+ * Custom render function for the drag ghost element.
46
+ * Replaces the default blue glowing box with your own component.
47
+ */
48
+ getGhostPreview?: (params: { action: TimelineAction; row: TimelineRow }) => React.ReactNode;
49
+ };
50
+
51
+ /** Edit area ref data */
52
+ export interface EditAreaState {
53
+ domRef: React.MutableRefObject<HTMLDivElement | null>;
54
+ }
55
+
56
+ // ─────────────────────────────────────────────
57
+ // Row-reorder drag state (existing feature)
58
+ // ─────────────────────────────────────────────
59
+
60
+ interface RowDragState {
61
+ isDragging: boolean;
62
+ draggedRow: TimelineRow | null;
63
+ draggedIndex: number;
64
+ targetIndex: number;
65
+ placeholderIndex: number;
66
+ originalData: TimelineRow[];
67
+ insertionLine: { visible: boolean; position: 'top' | 'bottom'; index: number };
68
+ dragPreview: { visible: boolean; top: number; left: number; width: number; height: number };
69
+ }
70
+
71
+ const initialRowDragState: RowDragState = {
72
+ isDragging: false,
73
+ draggedRow: null,
74
+ draggedIndex: -1,
75
+ targetIndex: -1,
76
+ placeholderIndex: -1,
77
+ originalData: [],
78
+ insertionLine: { visible: false, position: 'top', index: -1 },
79
+ dragPreview: { visible: false, top: 0, left: 0, width: 0, height: 0 },
80
+ };
81
+
82
+ // ─────────────────────────────────────────────
83
+ // Inner component (has access to CrossRowDragProvider context)
84
+ // ─────────────────────────────────────────────
85
+
86
+ const EditAreaInner = React.forwardRef<EditAreaState, EditAreaProps>((props, ref) => {
87
+ const {
88
+ editorData,
89
+ rowHeight,
90
+ scaleWidth,
91
+ scaleCount,
92
+ scaleSplitCount,
93
+ startLeft,
94
+ scrollLeft,
95
+ scrollTop,
96
+ scale,
97
+ gridSnap,
98
+ hideCursor = false,
99
+ cursorTime,
100
+ onScroll,
101
+ dragLine,
102
+ getAssistDragLineActionIds,
103
+ onActionMoveEnd,
104
+ onActionMoveStart,
105
+ onActionMoving,
106
+ onActionResizeEnd,
107
+ onActionResizeStart,
108
+ onActionResizing,
109
+ enableRowDrag = false,
110
+ onRowDragStart,
111
+ onRowDragEnd,
112
+ setEditorData,
113
+ enableCrossRowDrag = false,
114
+ enableGhostPreview = true,
115
+ getGhostPreview,
116
+ } = props;
117
+
118
+ const { dragLineData, initDragLine, updateDragLine, disposeDragLine, defaultGetAssistPosition, defaultGetMovePosition } =
119
+ useDragLine();
120
+
121
+ const editAreaRef = useRef<HTMLDivElement>(null);
122
+ const gridRef = useRef<Grid>(null);
123
+ const heightRef = useRef(-1);
124
+ const scrollToPositionRef = useRef({ scrollLeft, scrollTop });
125
+ const onScrollParamsRef = useRef<ScrollParams>({
126
+ clientHeight: 0,
127
+ clientWidth: 0,
128
+ scrollHeight: 0,
129
+ scrollLeft: 0,
130
+ scrollTop: 0,
131
+ scrollWidth: 0,
132
+ });
133
+
134
+ // Row reorder drag state
135
+ const [dragState, setDragState] = useState<RowDragState>(initialRowDragState);
136
+
137
+ // Cross-row drag context
138
+ const crossRowDrag = useCrossRowDrag();
139
+
140
+ // Ghost state (mirrors context state so we can render without extra subscription)
141
+ const [ghostState, setGhostState] = useState<CrossRowDragState>(crossRowDrag.state);
142
+
143
+ useEffect(() => {
144
+ setGhostState(crossRowDrag.state);
145
+ }, [crossRowDrag.state]);
146
+
147
+ // ─── ref ───
148
+ useImperativeHandle(ref, () => ({ get domRef() { return editAreaRef; } }));
149
+
150
+ // ─────────────────────────────────────────────
151
+ // Cross-row drag: resolve commit
152
+ // ─────────────────────────────────────────────
153
+
154
+ /**
155
+ * Given a clientX position, compute new start/end times for the action placed
156
+ * into the target row.
157
+ *
158
+ * When `gridSnap` is enabled the start time is rounded to the nearest grid
159
+ * unit (`scale / scaleSplitCount`) — the same snapping applied by `RowDnd`
160
+ * during same-row drags. The end time is always derived as `start + duration`
161
+ * so the block length is preserved exactly.
162
+ */
163
+ const resolveNewTimes = useCallback(
164
+ (clientX: number, action: TimelineAction, grabOffsetX: number): { newStart: number; newEnd: number } => {
165
+ if (!editAreaRef.current) return { newStart: action.start, newEnd: action.end };
166
+
167
+ const rect = editAreaRef.current.getBoundingClientRect();
168
+ // x relative to the edit area (accounting for current horizontal scroll)
169
+ const x = clientX - rect.left + scrollToPositionRef.current.scrollLeft - grabOffsetX;
170
+ const rawStart = parserPixelToTime(x, { startLeft, scale, scaleWidth });
171
+
172
+ // Apply the same grid-snap rounding that RowDnd uses during same-row drags
173
+ const gridUnit: number = scale / scaleSplitCount;
174
+ const snappedStart: number = gridSnap
175
+ ? Math.round(rawStart / gridUnit) * gridUnit
176
+ : rawStart;
177
+
178
+ const duration: number = action.end - action.start;
179
+ const newStart = Math.max(0, snappedStart);
180
+ const newEnd = Math.max(duration, newStart + duration);
181
+ return { newStart, newEnd };
182
+ },
183
+ [startLeft, scale, scaleWidth, gridSnap, scaleSplitCount],
184
+ );
185
+
186
+ /**
187
+ * Given a clientY, determine which row index the cursor is over.
188
+ */
189
+ const resolveTargetRow = useCallback(
190
+ (clientY: number): number => {
191
+ if (!editAreaRef.current) return -1;
192
+
193
+ const rect = editAreaRef.current.getBoundingClientRect();
194
+ const viewportY = clientY - rect.top + scrollToPositionRef.current.scrollTop;
195
+
196
+ let currentTop = 0;
197
+ for (let i = 0; i < editorData.length; i++) {
198
+ const rh = editorData[i]?.rowHeight || rowHeight;
199
+ if (viewportY >= currentTop && viewportY < currentTop + rh) return i;
200
+ currentTop += rh;
201
+ }
202
+ return editorData.length - 1;
203
+ },
204
+ [editorData, rowHeight],
205
+ );
206
+
207
+ // Register the commit handler into the cross-row context
208
+ useEffect(() => {
209
+ crossRowDrag.setOnCommit((action, sourceRow, _placeholder, clientX, clientY) => {
210
+ const targetRowIndex = resolveTargetRow(clientY as unknown as number);
211
+ if (targetRowIndex < 0) return;
212
+
213
+ const targetRow = editorData[targetRowIndex];
214
+ if (!targetRow) return;
215
+
216
+ // Don't move if dropped on the same row
217
+ if (targetRow.id === sourceRow.id) {
218
+ // Still update position within the same row from cursor X
219
+ const { newStart, newEnd } = resolveNewTimes(clientX as unknown as number, action, crossRowDrag.state.grabOffsetX);
220
+ const newData = editorData.map((r) => {
221
+ if (r.id !== sourceRow.id) return r;
222
+ return {
223
+ ...r,
224
+ actions: r.actions.map((a) => (a.id !== action.id ? a : { ...a, start: newStart, end: newEnd })),
225
+ };
226
+ });
227
+ setEditorData(newData);
228
+ return;
229
+ }
230
+
231
+ // Cross-row: remove from source, add to target
232
+ const { newStart, newEnd } = resolveNewTimes(clientX as unknown as number, action, crossRowDrag.state.grabOffsetX);
233
+ const updatedAction: TimelineAction = { ...action, start: newStart, end: newEnd };
234
+
235
+ const newData = editorData.map((r) => {
236
+ if (r.id === sourceRow.id) {
237
+ return { ...r, actions: r.actions.filter((a) => a.id !== action.id) };
238
+ }
239
+ if (r.id === targetRow.id) {
240
+ return { ...r, actions: [...r.actions, updatedAction] };
241
+ }
242
+ return r;
243
+ });
244
+
245
+ setEditorData(newData);
246
+ });
247
+ }, [crossRowDrag, editorData, resolveTargetRow, resolveNewTimes, setEditorData]);
248
+
249
+ // ─────────────────────────────────────────────
250
+ // Row reorder drag (existing feature)
251
+ // ─────────────────────────────────────────────
252
+
253
+ const reorderRows = useCallback((data: TimelineRow[], fromIndex: number, toIndex: number): TimelineRow[] => {
254
+ const result = [...data];
255
+ if (toIndex > fromIndex && toIndex < data.length) toIndex = toIndex - 1;
256
+ const [removed] = result.splice(fromIndex, 1);
257
+ result.splice(toIndex, 0, removed);
258
+ return result;
259
+ }, []);
260
+
261
+ const calculateTargetIndex = useCallback(
262
+ (clientY: number): { index: number; position: 'top' | 'bottom' } => {
263
+ if (!editAreaRef.current) return { index: -1, position: 'top' };
264
+ const rect = editAreaRef.current.getBoundingClientRect();
265
+ const viewportTop = clientY - rect.top + scrollToPositionRef.current.scrollTop;
266
+ const rowCount = editorData.length;
267
+ let currentTop = 0;
268
+ if (rowCount > 0 && viewportTop < 0) return { index: 0, position: 'top' };
269
+ for (let i = 0; i < rowCount; i++) {
270
+ const rh = editorData[i]?.rowHeight || rowHeight;
271
+ const bottom = currentTop + rh;
272
+ if (viewportTop >= currentTop && viewportTop <= bottom) {
273
+ return Math.abs(viewportTop - currentTop) < Math.abs(viewportTop - bottom)
274
+ ? { index: i, position: 'top' }
275
+ : { index: i + 1, position: 'top' };
276
+ }
277
+ currentTop = bottom;
278
+ }
279
+ return { index: rowCount, position: 'top' };
280
+ },
281
+ [editorData, rowHeight],
282
+ );
283
+
284
+ const calculateDragPreviewPosition = useCallback(
285
+ (clientY: number, previewHeight: number) => {
286
+ if (!editAreaRef.current) return 0;
287
+ const rect = editAreaRef.current.getBoundingClientRect();
288
+ let top = clientY - rect.top - previewHeight / 2;
289
+ top = Math.max(0, Math.min(top, rect.height - previewHeight));
290
+ return top;
291
+ },
292
+ [scrollTop],
293
+ );
294
+
295
+ const calculateRowAccumulatedHeight = useCallback(
296
+ (rowIndex: number): number => {
297
+ let h = 0;
298
+ for (let i = 0; i < rowIndex; i++) h += editorData[i]?.rowHeight || rowHeight;
299
+ return Math.max(0, h - scrollTop);
300
+ },
301
+ [editorData, scrollTop, rowHeight],
302
+ );
303
+
304
+ const initializeDragState = useCallback(
305
+ (row: TimelineRow, rowIndex: number) => {
306
+ const originalData = [...editorData];
307
+ const ph = row.rowHeight || rowHeight;
308
+ return {
309
+ isDragging: true,
310
+ draggedRow: row,
311
+ draggedIndex: rowIndex,
312
+ targetIndex: -1,
313
+ placeholderIndex: rowIndex,
314
+ originalData,
315
+ insertionLine: { visible: false, position: 'top' as const, index: -1 },
316
+ dragPreview: { visible: true, top: calculateRowAccumulatedHeight(rowIndex), left: 0, width: 0, height: ph },
317
+ };
318
+ },
319
+ [editorData, rowHeight, calculateRowAccumulatedHeight],
320
+ );
321
+
322
+ const updateRowDragState = useCallback(
323
+ (currentState: RowDragState, targetIndex: number, previewTop: number, rowIndex: number) => {
324
+ if (currentState.targetIndex === targetIndex && currentState.dragPreview.top === previewTop)
325
+ return currentState;
326
+ return {
327
+ ...currentState,
328
+ targetIndex,
329
+ placeholderIndex: targetIndex > rowIndex ? targetIndex - 1 : targetIndex,
330
+ insertionLine: {
331
+ visible: targetIndex !== -1 && targetIndex !== rowIndex,
332
+ position: 'top' as const,
333
+ index: targetIndex,
334
+ },
335
+ dragPreview: { ...currentState.dragPreview, top: previewTop },
336
+ };
337
+ },
338
+ [],
339
+ );
340
+
341
+ const handleRowDragEnd = useCallback(
342
+ (draggedIndex: number, targetIndex: number, draggedRow: TimelineRow | null, originalData: TimelineRow[]) => {
343
+ if (isValidDragTarget(targetIndex, draggedIndex, editorData.length)) {
344
+ const newData = reorderRows(editorData, draggedIndex, targetIndex);
345
+ setEditorData(newData);
346
+ onRowDragEnd?.({ row: draggedRow!, editorData: newData });
347
+ } else {
348
+ setEditorData(originalData);
349
+ }
350
+ },
351
+ [editorData, reorderRows, setEditorData, onRowDragEnd],
352
+ );
353
+
354
+ const handleRowDragStart = useCallback(
355
+ (row: TimelineRow, rowIndex: number) => {
356
+ if (!enableRowDrag) return;
357
+ const initState = initializeDragState(row, rowIndex);
358
+ setDragState(initState);
359
+ onRowDragStart?.({ row });
360
+
361
+ let animationFrameId: number | null = null;
362
+ let autoScrollAnimationFrameId: number | null = null;
363
+ let lastUpdateTime = 0;
364
+ const UPDATE_INTERVAL = 16;
365
+ let currentMouseY = 0;
366
+ let isDraggingLocal = false;
367
+
368
+ const updateDragPosition = (mouseY: number) => {
369
+ const now = Date.now();
370
+ if (now - lastUpdateTime >= UPDATE_INTERVAL) {
371
+ const targetInfo = calculateTargetIndex(mouseY);
372
+ const previewTop = calculateDragPreviewPosition(mouseY, initState.dragPreview.height);
373
+ setDragState((prev) => updateRowDragState(prev, targetInfo.index, previewTop, rowIndex));
374
+ lastUpdateTime = now;
375
+ }
376
+ };
377
+
378
+ const checkAndScroll = () => {
379
+ if (!isDraggingLocal) return (autoScrollAnimationFrameId = requestAnimationFrame(checkAndScroll)) as unknown as void;
380
+
381
+ if (editAreaRef.current && gridRef.current) {
382
+ const gridEl = editAreaRef.current.querySelector('.ReactVirtualized__Grid') as HTMLElement | null;
383
+ if (gridEl) {
384
+ const gr = gridEl.getBoundingClientRect();
385
+ const mouseY = currentMouseY - gr.top;
386
+ const threshold = 50;
387
+ const speed = 10;
388
+ if (mouseY < threshold) {
389
+ onScroll({ ...onScrollParamsRef.current, scrollTop: Math.max(0, gridEl.scrollTop - speed) });
390
+ updateDragPosition(currentMouseY);
391
+ } else if (mouseY > gr.height - threshold) {
392
+ onScroll({
393
+ ...onScrollParamsRef.current,
394
+ scrollTop: Math.min(gridEl.scrollTop + speed, gridEl.scrollHeight - gr.height),
395
+ });
396
+ updateDragPosition(currentMouseY);
397
+ }
398
+ }
399
+ }
400
+ autoScrollAnimationFrameId = requestAnimationFrame(checkAndScroll);
401
+ };
402
+
403
+ autoScrollAnimationFrameId = requestAnimationFrame(checkAndScroll);
404
+
405
+ const handleMouseMove = (me: MouseEvent) => {
406
+ isDraggingLocal = true;
407
+ currentMouseY = me.clientY;
408
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
409
+ animationFrameId = requestAnimationFrame(() => {
410
+ updateDragPosition(currentMouseY);
411
+ animationFrameId = null;
412
+ });
413
+ };
414
+
415
+ const handleMouseUp = () => {
416
+ isDraggingLocal = false;
417
+ if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; }
418
+ if (autoScrollAnimationFrameId) { cancelAnimationFrame(autoScrollAnimationFrameId); autoScrollAnimationFrameId = null; }
419
+
420
+ setDragState((prevState) => {
421
+ const { draggedIndex, targetIndex, originalData, draggedRow } = prevState;
422
+ setTimeout(() => handleRowDragEnd(draggedIndex, targetIndex, draggedRow, originalData), 0);
423
+ return {
424
+ ...initialRowDragState,
425
+ dragPreview: { ...initialRowDragState.dragPreview, visible: false },
426
+ insertionLine: { ...initialRowDragState.insertionLine, visible: false },
427
+ };
428
+ });
429
+
430
+ document.removeEventListener('mousemove', handleMouseMove);
431
+ document.removeEventListener('mouseup', handleMouseUp);
432
+ };
433
+
434
+ document.addEventListener('mousemove', handleMouseMove);
435
+ document.addEventListener('mouseup', handleMouseUp);
436
+ },
437
+ [enableRowDrag, onRowDragStart, initializeDragState, calculateTargetIndex, calculateDragPreviewPosition, updateRowDragState, handleRowDragEnd, scrollTop],
438
+ );
439
+
440
+ // ─────────────────────────────────────────────
441
+ // Drag line (same-row assist lines)
442
+ // ─────────────────────────────────────────────
443
+
444
+ const handleInitDragLine: EditData['onActionMoveStart'] = (data) => {
445
+ if (dragLine) {
446
+ const assistActionIds =
447
+ getAssistDragLineActionIds &&
448
+ getAssistDragLineActionIds({ action: data.action, row: data.row, editorData });
449
+ const cursorLeft = parserTimeToPixel(cursorTime, { scaleWidth, scale, startLeft });
450
+ const assistPositions = defaultGetAssistPosition({
451
+ editorData,
452
+ assistActionIds,
453
+ action: data.action,
454
+ row: data.row,
455
+ scale,
456
+ scaleWidth,
457
+ startLeft,
458
+ hideCursor,
459
+ cursorLeft,
460
+ });
461
+ initDragLine({ assistPositions });
462
+ }
463
+ };
464
+
465
+ const handleUpdateDragLine: EditData['onActionMoving'] = (data) => {
466
+ if (dragLine) {
467
+ const movePositions = defaultGetMovePosition({ ...data, startLeft, scaleWidth, scale });
468
+ updateDragLine({ movePositions });
469
+ }
470
+ };
471
+
472
+ // ─────────────────────────────────────────────
473
+ // Cell renderer
474
+ // ─────────────────────────────────────────────
475
+
476
+ const cellRenderer: GridCellRenderer = ({ rowIndex, key, style }) => {
477
+ const row = editorData[rowIndex];
478
+ return (
479
+ <EditRow
480
+ {...props}
481
+ style={{
482
+ ...style,
483
+ backgroundPositionX: `0, ${startLeft}px`,
484
+ backgroundSize: `${startLeft}px, ${scaleWidth}px`,
485
+ }}
486
+ areaRef={editAreaRef}
487
+ key={key}
488
+ rowHeight={row?.rowHeight || rowHeight}
489
+ rowData={row}
490
+ dragLineData={dragLineData}
491
+ rowIndex={rowIndex}
492
+ dragState={{ isDragging: dragState.draggedIndex !== -1, draggedIndex: dragState.draggedIndex }}
493
+ enableCrossRowDrag={enableCrossRowDrag}
494
+ enableGhostPreview={enableGhostPreview}
495
+ onRowDragStart={(params) => handleRowDragStart(params.row, rowIndex)}
496
+ onActionMoveStart={(data) => {
497
+ handleInitDragLine(data);
498
+ return onActionMoveStart && onActionMoveStart(data);
499
+ }}
500
+ onActionResizeStart={(data) => {
501
+ handleInitDragLine(data);
502
+ return onActionResizeStart && onActionResizeStart(data);
503
+ }}
504
+ onActionMoving={(data) => {
505
+ handleUpdateDragLine(data);
506
+ return onActionMoving && onActionMoving(data);
507
+ }}
508
+ onActionResizing={(data) => {
509
+ handleUpdateDragLine(data);
510
+ return onActionResizing && onActionResizing(data);
511
+ }}
512
+ onActionResizeEnd={(data) => {
513
+ disposeDragLine();
514
+ return onActionResizeEnd && onActionResizeEnd(data);
515
+ }}
516
+ onActionMoveEnd={(data) => {
517
+ disposeDragLine();
518
+ return onActionMoveEnd && onActionMoveEnd(data);
519
+ }}
520
+ />
521
+ );
522
+ };
523
+
524
+ // ─────────────────────────────────────────────
525
+ // Scroll sync
526
+ // ─────────────────────────────────────────────
527
+
528
+ useLayoutEffect(() => {
529
+ gridRef.current?.scrollToPosition({ scrollTop, scrollLeft });
530
+ scrollToPositionRef.current = { scrollTop, scrollLeft };
531
+ }, [scrollTop, scrollLeft]);
532
+
533
+ useEffect(() => {
534
+ gridRef.current?.recomputeGridSize();
535
+ }, [editorData]);
536
+
537
+ // ─────────────────────────────────────────────
538
+ // Render
539
+ // ─────────────────────────────────────────────
540
+
541
+ return (
542
+ <div ref={editAreaRef} className={prefix('edit-area')}>
543
+ <AutoSizer>
544
+ {({ width, height }) => {
545
+ const totalHeight = calculateTotalHeight(editorData, rowHeight);
546
+ const heights = getRowHeights(editorData, rowHeight);
547
+ if (totalHeight < height) {
548
+ heights.push(height - totalHeight);
549
+ if (heightRef.current !== height && heightRef.current >= 0) {
550
+ setTimeout(() =>
551
+ gridRef.current?.recomputeGridSize({ rowIndex: heights.length - 1 }),
552
+ );
553
+ }
554
+ }
555
+ heightRef.current = height;
556
+
557
+ return (
558
+ <>
559
+ <Grid
560
+ columnCount={1}
561
+ rowCount={heights.length}
562
+ ref={gridRef}
563
+ cellRenderer={cellRenderer}
564
+ columnWidth={Math.max(scaleCount * scaleWidth + startLeft, width)}
565
+ width={width}
566
+ height={height}
567
+ rowHeight={({ index }) => heights[index] || rowHeight}
568
+ overscanRowCount={10}
569
+ overscanColumnCount={0}
570
+ onScroll={(param) => {
571
+ onScrollParamsRef.current = param;
572
+ onScroll(param);
573
+ }}
574
+ />
575
+ {/* Row insertion line (row reorder) */}
576
+ <InsertionLine top={calculateRowAccumulatedHeight(dragState.insertionLine.index)} visible={dragState.insertionLine.visible} />
577
+ {/* Row drag preview (row reorder) */}
578
+ <DragPreview top={dragState.dragPreview.top} height={dragState.dragPreview.height} visible={dragState.dragPreview.visible} />
579
+ {/* Block ghost preview (cross-row block drag) */}
580
+ <CrossRowGhost
581
+ state={ghostState}
582
+ enableGhostPreview={enableGhostPreview}
583
+ getGhostPreview={getGhostPreview}
584
+ />
585
+ </>
586
+ );
587
+ }}
588
+ </AutoSizer>
589
+ {dragLine && <DragLines scrollLeft={scrollLeft} {...dragLineData} />}
590
+ </div>
591
+ );
592
+ });
593
+
594
+ EditAreaInner.displayName = 'EditAreaInner';
595
+
596
+ // ─────────────────────────────────────────────
597
+ // Public export — wraps inner with provider
598
+ // ─────────────────────────────────────────────
599
+
600
+ export const EditArea = React.forwardRef<EditAreaState, EditAreaProps>((props, ref) => (
601
+ <CrossRowDragProvider>
602
+ <EditAreaInner {...props} ref={ref} />
603
+ </CrossRowDragProvider>
604
+ ));
605
+
606
+ EditArea.displayName = 'EditArea';
@@ -0,0 +1,78 @@
1
+ .timeline-editor-edit-row-dragging {
2
+ opacity: 0.5;
3
+ }
4
+ .timeline-editor-edit-row {
5
+ background-repeat: no-repeat, repeat;
6
+ background-image: linear-gradient(#191b1d, #191b1d), linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 0);
7
+ display: flex;
8
+ flex-direction: row;
9
+ box-sizing: border-box;
10
+ position: relative;
11
+ transition: opacity 0.2s ease, transform 0.2s ease;
12
+ }
13
+ .timeline-editor-edit-row-drag-handle {
14
+ position: absolute;
15
+ left: 4px;
16
+ top: 50%;
17
+ transform: translateY(-50%);
18
+ width: 16px;
19
+ height: 16px;
20
+ cursor: grab;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ color: #666;
25
+ font-size: 12px;
26
+ user-select: none;
27
+ z-index: 10;
28
+ opacity: 0.6;
29
+ transition: opacity 0.2s ease;
30
+ }
31
+ .timeline-editor-edit-row-drag-handle:hover {
32
+ opacity: 1;
33
+ color: #999;
34
+ }
35
+ .timeline-editor-edit-row-drag-handle:active {
36
+ cursor: grabbing;
37
+ }
38
+ .timeline-editor-edit-row-dragged {
39
+ opacity: 0.5;
40
+ transform: scale(0.98);
41
+ pointer-events: none;
42
+ z-index: 1000;
43
+ position: relative;
44
+ }
45
+ .timeline-editor-edit-row-dragging {
46
+ cursor: grabbing;
47
+ }
48
+ .timeline-editor-edit-row-placeholder {
49
+ background: rgba(74, 144, 226, 0.1);
50
+ border: 2px dashed #4a90e2;
51
+ height: 4px;
52
+ min-height: 4px;
53
+ margin: 2px 0;
54
+ opacity: 0.8;
55
+ }
56
+ .timeline-editor-edit-row-placeholder .timeline-editor-edit-row-drag-handle,
57
+ .timeline-editor-edit-row-placeholder .timeline-editor-edit-action {
58
+ display: none;
59
+ }
60
+ .timeline-editor-edit-row::before {
61
+ content: '';
62
+ position: absolute;
63
+ left: 0;
64
+ right: 0;
65
+ height: 2px;
66
+ background: #4a90e2;
67
+ z-index: 100;
68
+ opacity: 0;
69
+ transition: opacity 0.2s ease;
70
+ }
71
+ .timeline-editor-edit-row[data-insert-position="top"]::before {
72
+ top: -1px;
73
+ opacity: 1;
74
+ }
75
+ .timeline-editor-edit-row[data-insert-position="bottom"]::before {
76
+ bottom: -1px;
77
+ opacity: 1;
78
+ }