@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,329 @@
1
+ import { Emitter, EventTypes, ITimelineEngine } from '@keplar-404/timeline-engine';
2
+ import React, { ReactNode } from 'react';
3
+ import { OnScrollParams } from 'react-virtualized';
4
+ import { TimelineAction, TimelineRow } from '@keplar-404/timeline-engine';
5
+ import { TimelineEffect } from '@keplar-404/timeline-engine';
6
+
7
+ export interface EditData {
8
+ /**
9
+ * @description Timeline editing data
10
+ */
11
+ editorData: TimelineRow[];
12
+ /**
13
+ * @description Timeline action effect map
14
+ */
15
+ effects: Record<string, TimelineEffect>;
16
+ /**
17
+ * @description Scaling factor for individual scale marks (>0)
18
+ * @default 1
19
+ */
20
+ scale?: number;
21
+ /**
22
+ * @description Minimum number of scale marks (>=1)
23
+ * @default 20
24
+ */
25
+ minScaleCount?: number;
26
+ /**
27
+ * @description Maximum number of scale marks (>=minScaleCount)
28
+ * @default Infinity
29
+ */
30
+ maxScaleCount?: number;
31
+ /**
32
+ * @description Number of subdivision units for a single scale mark (integer >0)
33
+ * @default 10
34
+ */
35
+ scaleSplitCount?: number;
36
+ /**
37
+ * @description Display width of a single scale mark (>0, unit: px)
38
+ * @default 160
39
+ */
40
+ scaleWidth?: number;
41
+ /**
42
+ * @description Distance from the start of the scale to the left edge (>=0, unit: px)
43
+ * @default 20
44
+ */
45
+ startLeft?: number;
46
+ /**
47
+ * @description Default height for each editing row (>0, unit: px)
48
+ * @default 32
49
+ */
50
+ rowHeight?: number;
51
+ /**
52
+ * @description Whether to enable grid movement snapping
53
+ * @default false
54
+ */
55
+ gridSnap?: boolean;
56
+ /**
57
+ * @description Enable snapping to drag auxiliary lines
58
+ * @default false
59
+ */
60
+ dragLine?: boolean;
61
+ /**
62
+ * @description Whether to hide the cursor
63
+ * @default false
64
+ */
65
+ hideCursor?: boolean;
66
+ /**
67
+ * @description Prevent dragging in all action areas
68
+ * @default false
69
+ */
70
+ disableDrag?: boolean;
71
+ /**
72
+ * @description Prevent dragging of all rows
73
+ * @default false
74
+ */
75
+ enableRowDrag?: boolean;
76
+ /**
77
+ * @description Enable dragging action blocks between different rows
78
+ * @default false
79
+ */
80
+ enableCrossRowDrag?: boolean;
81
+ /**
82
+ * @description Show a ghost/preview element following the cursor during cross-row block drag.
83
+ * Set to `false` to disable the ghost entirely, or use `getGhostPreview` for a custom component.
84
+ * @default true
85
+ */
86
+ enableGhostPreview?: boolean;
87
+ /**
88
+ * @description Custom render function for the drag ghost/preview element shown while
89
+ * dragging a block across rows. When provided, replaces the default blue glowing box.
90
+ *
91
+ * The function receives the action being dragged and its source row, so you can
92
+ * render a fully custom preview that matches your block's actual appearance.
93
+ *
94
+ * @param params - The action being dragged and the row it originated from
95
+ * @returns A React node to render inside the ghost container
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * <Timeline
100
+ * enableCrossRowDrag
101
+ * getGhostPreview={({ action, row }) => (
102
+ * <div style={{ background: '#1a3a5c', border: '2px solid #3b82f6', height: '100%', borderRadius: 4, padding: '0 8px', display: 'flex', alignItems: 'center' }}>
103
+ * <span style={{ color: '#3b82f6', fontSize: 12 }}>{action.id}</span>
104
+ * </div>
105
+ * )}
106
+ * />
107
+ * ```
108
+ */
109
+ getGhostPreview?: (params: { action: TimelineAction; row: TimelineRow }) => ReactNode;
110
+ /**
111
+ * @description Timeline engine; uses the built-in engine if not provided
112
+ */
113
+ engine?: ITimelineEngine;
114
+ /**
115
+ * @description Custom action area rendering
116
+ */
117
+ getActionRender?: (action: TimelineAction, row: TimelineRow) => ReactNode;
118
+ /**
119
+ * @description Custom scale rendering
120
+ */
121
+ getScaleRender?: (scale: number) => ReactNode;
122
+ /**
123
+ * @description Callback when movement starts
124
+ */
125
+ onActionMoveStart?: (params: { action: TimelineAction; row: TimelineRow }) => void;
126
+ /**
127
+ * @description Movement callback (return false to prevent movement)
128
+ */
129
+ onActionMoving?: (params: { action: TimelineAction; row: TimelineRow; start: number; end: number }) => void | boolean;
130
+ /**
131
+ * @description Movement end callback (return false to prevent onChange from triggering)
132
+ */
133
+ onActionMoveEnd?: (params: { action: TimelineAction; row: TimelineRow; start: number; end: number }) => void;
134
+ /**
135
+ * @description Callback when resizing starts
136
+ */
137
+ onActionResizeStart?: (params: { action: TimelineAction; row: TimelineRow; dir: 'right' | 'left' }) => void;
138
+ /**
139
+ * @description Resizing callback (return false to prevent change)
140
+ */
141
+ onActionResizing?: (params: { action: TimelineAction; row: TimelineRow; start: number; end: number; dir: 'right' | 'left' }) => void | boolean;
142
+ /**
143
+ * @description Callback when resizing ends (return false to prevent onChange from triggering)
144
+ */
145
+ onActionResizeEnd?: (params: { action: TimelineAction; row: TimelineRow; start: number; end: number; dir: 'right' | 'left' }) => void;
146
+ /**
147
+ * @description Callback when a row is clicked
148
+ */
149
+ onClickRow?: (
150
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
151
+ param: {
152
+ row: TimelineRow;
153
+ time: number;
154
+ },
155
+ ) => void;
156
+ /**
157
+ * @description Callback when an action is clicked
158
+ */
159
+ onClickAction?: (
160
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
161
+ param: {
162
+ action: TimelineAction;
163
+ row: TimelineRow;
164
+ time: number;
165
+ },
166
+ ) => void;
167
+ /**
168
+ * @description Callback when an action is clicked (not executed when drag is triggered)
169
+ */
170
+ onClickActionOnly?: (
171
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
172
+ param: {
173
+ action: TimelineAction;
174
+ row: TimelineRow;
175
+ time: number;
176
+ },
177
+ ) => void;
178
+ /**
179
+ * @description Callback when a row is double-clicked
180
+ */
181
+ onDoubleClickRow?: (
182
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
183
+ param: {
184
+ row: TimelineRow;
185
+ time: number;
186
+ },
187
+ ) => void;
188
+ /**
189
+ * @description Callback when an action is double-clicked
190
+ */
191
+ onDoubleClickAction?: (
192
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
193
+ param: {
194
+ action: TimelineAction;
195
+ row: TimelineRow;
196
+ time: number;
197
+ },
198
+ ) => void;
199
+ /**
200
+ * @description Callback when a row is right-clicked
201
+ */
202
+ onContextMenuRow?: (
203
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
204
+ param: {
205
+ row: TimelineRow;
206
+ time: number;
207
+ },
208
+ ) => void;
209
+ /**
210
+ * @description Callback when an action is right-clicked
211
+ */
212
+ onContextMenuAction?: (
213
+ e: React.MouseEvent<HTMLElement, MouseEvent>,
214
+ param: {
215
+ action: TimelineAction;
216
+ row: TimelineRow;
217
+ time: number;
218
+ },
219
+ ) => void;
220
+ /**
221
+ * @description Get a list of action IDs for auxiliary lines, calculated at move/resize start; defaults to all except the current moving action
222
+ */
223
+ getAssistDragLineActionIds?: (params: { action: TimelineAction; editorData: TimelineRow[]; row: TimelineRow }) => string[];
224
+ /**
225
+ * @description Cursor start drag event
226
+ */
227
+ onCursorDragStart?: (time: number) => void;
228
+ /**
229
+ * @description Cursor end drag event
230
+ */
231
+ onCursorDragEnd?: (time: number) => void;
232
+ /**
233
+ * @description Cursor drag event
234
+ */
235
+ onCursorDrag?: (time: number) => void;
236
+ /**
237
+ * @description Click on time area event; return false to prevent setting time
238
+ */
239
+ onClickTimeArea?: (time: number, e: React.MouseEvent<HTMLDivElement, MouseEvent>) => boolean | undefined;
240
+ /**
241
+ * @description Row drag start callback
242
+ * @param params row is the data of the row being dragged
243
+ */
244
+ onRowDragStart?: (params: { row: TimelineRow }) => void;
245
+ /**
246
+ * @description Row drag end callback
247
+ * @param params row is the data of the row being dragged; editorData is the new data arrangement after the row is dragged
248
+ * @returns
249
+ */
250
+ onRowDragEnd?: (params: { row: TimelineRow; editorData: TimelineRow[] }) => void;
251
+ }
252
+
253
+ export interface TimelineState {
254
+ /** DOM node */
255
+ target: HTMLElement | null;
256
+ /** Execution listener */
257
+ listener: Emitter<EventTypes>;
258
+ /** Whether it is playing */
259
+ isPlaying: boolean;
260
+ /** Whether it is paused */
261
+ isPaused: boolean;
262
+ /** Set current playback time */
263
+ setTime: (time: number) => void;
264
+ /** Get current playback time */
265
+ getTime: () => number;
266
+ /** Set playback rate */
267
+ setPlayRate: (rate: number) => void;
268
+ /** Get playback rate */
269
+ getPlayRate: () => number;
270
+ /** Re-render current time */
271
+ reRender: () => void;
272
+ /** Play */
273
+ play: (param: {
274
+ /** Default run from start to end, priority greater than autoEnd */
275
+ toTime?: number;
276
+ /** Whether it is automatically end after playback */
277
+ autoEnd?: boolean;
278
+ /** List of actionIds to run; runs all by default if not provided */
279
+ runActionIds?: string[];
280
+ }) => boolean;
281
+ /** Pause */
282
+ pause: () => void;
283
+ /** Set scroll left */
284
+ setScrollLeft: (val: number) => void;
285
+ /** Set scroll top */
286
+ setScrollTop: (val: number) => void;
287
+ }
288
+
289
+ /**
290
+ * Animation editor parameters
291
+ * @export
292
+ * @interface TimelineProp
293
+ */
294
+ export interface TimelineEditor extends EditData {
295
+ /**
296
+ * @description Scroll distance of the editing area from the top (please use ref.setScrollTop instead)
297
+ * @deprecated
298
+ */
299
+ scrollTop?: number;
300
+ /**
301
+ * @description Scroll callback for the editing area (used to control synchronization with scroll of editing rows)
302
+ */
303
+ onScroll?: (params: OnScrollParams) => void;
304
+ /**
305
+ * @description Whether to enable automatic scrolling during dragging
306
+ * @default false
307
+ */
308
+ autoScroll?: boolean;
309
+ /**
310
+ * @description Custom timeline style
311
+ */
312
+ style?: React.CSSProperties;
313
+ /**
314
+ * @description Whether to re-render automatically (update tick when data changes or cursor time changes)
315
+ * @default true
316
+ */
317
+ autoReRender?: boolean;
318
+ /**
319
+ * @description Data change callback, triggered after the end of an action operation changes data (returning false will prevent automatic engine synchronization, used to reduce performance overhead)
320
+ */
321
+ onChange?: (editorData: TimelineRow[]) => void | boolean;
322
+ }
323
+
324
+ // Define a utility type to make specified properties required
325
+ export type RequiredPick<T, K extends keyof T> = T & { [P in K]-?: T[P] };
326
+
327
+ export type RequiredEditData = RequiredPick<EditData, 'editorData' | 'effects' | 'scale' | 'scaleSplitCount' | 'scaleWidth' | 'startLeft' | 'minScaleCount' | 'maxScaleCount' | 'rowHeight'>;
328
+
329
+ export type RequiredTimelineEditor = RequiredPick<TimelineEditor, 'scrollTop'> & RequiredEditData;
@@ -0,0 +1,77 @@
1
+ import { DEFAULT_ROW_HEIGHT, DEFAULT_SCALE, DEFAULT_SCALE_SPLIT_COUNT, DEFAULT_SCALE_WIDTH, DEFAULT_START_LEFT, MIN_SCALE_COUNT } from '../interface/const';
2
+ import { RequiredTimelineEditor, TimelineEditor } from '../interface/timeline';
3
+ import ConsoleLogger from './logger';
4
+ const logger = new ConsoleLogger('timeline');
5
+
6
+ export function checkProps(props: TimelineEditor): RequiredTimelineEditor {
7
+ let {
8
+ editorData = [],
9
+ effects = {},
10
+ scrollTop = 0,
11
+ scale = DEFAULT_SCALE,
12
+ scaleSplitCount = DEFAULT_SCALE_SPLIT_COUNT,
13
+ scaleWidth = DEFAULT_SCALE_WIDTH,
14
+ startLeft = DEFAULT_START_LEFT,
15
+ minScaleCount = MIN_SCALE_COUNT,
16
+ maxScaleCount = Infinity,
17
+ rowHeight = DEFAULT_ROW_HEIGHT,
18
+ } = props;
19
+
20
+ if (scale <= 0) {
21
+ logger.error('Error: scale must be greater than 0!');
22
+ scale = DEFAULT_SCALE;
23
+ }
24
+
25
+ if (scrollTop < 0) {
26
+ logger.warn('Warning: scrollTop cannot be less than 0!');
27
+ scrollTop = 0;
28
+ }
29
+
30
+ if (scaleSplitCount <= 0) {
31
+ logger.warn('Warning: scaleSplitCount cannot be less than 1!');
32
+ scaleSplitCount = 1;
33
+ }
34
+
35
+ if (scaleWidth <= 0) {
36
+ logger.warn('Warning: scaleWidth must be greater than 0!');
37
+ scaleWidth = DEFAULT_SCALE_WIDTH;
38
+ }
39
+
40
+ if (startLeft < 0) {
41
+ logger.warn('Warning: startLeft cannot be less than 0!');
42
+ startLeft = 0;
43
+ }
44
+
45
+ if (minScaleCount < 1) {
46
+ logger.warn('Warning: minScaleCount must be greater than 1!');
47
+ minScaleCount = MIN_SCALE_COUNT;
48
+ }
49
+ minScaleCount = parseInt(minScaleCount + '');
50
+
51
+ if (maxScaleCount < minScaleCount) {
52
+ logger.warn('Warning: maxScaleCount cannot be less than minScaleCount!');
53
+ maxScaleCount = minScaleCount;
54
+ }
55
+ maxScaleCount = maxScaleCount === Infinity ? Infinity : parseInt(maxScaleCount + '');
56
+
57
+ if (rowHeight <= 0) {
58
+ logger.warn('Warning: rowHeight must be greater than 0!');
59
+ rowHeight = DEFAULT_ROW_HEIGHT;
60
+ }
61
+
62
+ const temp = { ...props };
63
+ delete temp['style'];
64
+ return {
65
+ ...temp,
66
+ editorData,
67
+ effects,
68
+ scrollTop,
69
+ scale,
70
+ scaleSplitCount,
71
+ scaleWidth,
72
+ startLeft,
73
+ minScaleCount,
74
+ maxScaleCount,
75
+ rowHeight,
76
+ };
77
+ }
@@ -0,0 +1,6 @@
1
+ import { prefixNames } from "framework-utils";
2
+ import { PREFIX } from "../interface/const";
3
+
4
+ export function prefix(...classNames: string[]) {
5
+ return prefixNames(`${PREFIX}-`, ...classNames);
6
+ }
@@ -0,0 +1,159 @@
1
+ import { TimelineAction, TimelineRow } from "@keplar-404/timeline-engine";
2
+ import { ADD_SCALE_COUNT } from "../interface/const";
3
+
4
+ /** Time to pixel */
5
+ export function parserTimeToPixel(
6
+ data: number,
7
+ param: {
8
+ startLeft: number;
9
+ scale: number;
10
+ scaleWidth: number;
11
+ }
12
+ ) {
13
+ const { startLeft, scale, scaleWidth } = param;
14
+ return startLeft + (data / scale) * scaleWidth;
15
+ }
16
+
17
+ /** Pixel to time */
18
+ export function parserPixelToTime(
19
+ data: number,
20
+ param: {
21
+ startLeft: number;
22
+ scale: number;
23
+ scaleWidth: number;
24
+ }
25
+ ) {
26
+ const { startLeft, scale, scaleWidth } = param;
27
+ return ((data - startLeft) / scaleWidth) * scale;
28
+ }
29
+
30
+ /** Position + Width to Start + End */
31
+ export function parserTransformToTime(
32
+ data: {
33
+ left: number;
34
+ width: number;
35
+ },
36
+ param: {
37
+ startLeft: number;
38
+ scale: number;
39
+ scaleWidth: number;
40
+ }
41
+ ) {
42
+ const { left, width } = data;
43
+ const start = parserPixelToTime(left, param);
44
+ const end = parserPixelToTime(left + width, param);
45
+ return {
46
+ start,
47
+ end,
48
+ };
49
+ }
50
+
51
+ /** Start + End to Position + Width */
52
+ export function parserTimeToTransform(
53
+ data: {
54
+ start: number;
55
+ end: number;
56
+ },
57
+ param: {
58
+ startLeft: number;
59
+ scale: number;
60
+ scaleWidth: number;
61
+ }
62
+ ) {
63
+ const { start, end } = data;
64
+ const left = parserTimeToPixel(start, param);
65
+ const width = parserTimeToPixel(end, param) - left;
66
+ return {
67
+ left,
68
+ width,
69
+ };
70
+ }
71
+
72
+ /** Get number of scale marks based on data */
73
+ export function getScaleCountByRows(data: TimelineRow[], param: { scale: number }) {
74
+ let max = 0;
75
+ data.forEach((row) => {
76
+ row.actions.forEach((action: TimelineAction) => {
77
+ max = Math.max(max, action.end);
78
+ });
79
+ });
80
+ const count = Math.ceil(max / param.scale);
81
+ return count + ADD_SCALE_COUNT;
82
+ }
83
+
84
+ /** Get current number of scale marks based on time */
85
+ export function getScaleCountByPixel(
86
+ data: number,
87
+ param: {
88
+ startLeft: number;
89
+ scaleWidth: number;
90
+ scaleCount: number;
91
+ }
92
+ ) {
93
+ const { startLeft, scaleWidth } = param;
94
+ const count = Math.ceil((data - startLeft) / scaleWidth);
95
+ return Math.max(count + ADD_SCALE_COUNT, param.scaleCount);
96
+ }
97
+
98
+ /** Get collection of positions for all action times */
99
+ export function parserActionsToPositions(
100
+ actions: TimelineAction[],
101
+ param: {
102
+ startLeft: number;
103
+ scale: number;
104
+ scaleWidth: number;
105
+ }
106
+ ) {
107
+ const positions: number[] = [];
108
+ actions.forEach((item) => {
109
+ positions.push(parserTimeToPixel(item.start, param));
110
+ positions.push(parserTimeToPixel(item.end, param));
111
+ });
112
+ return positions;
113
+ }
114
+
115
+ /**
116
+ * Split an action in a row at a given time.
117
+ * Automatically updates start/end times and creates a new adjacent action.
118
+ */
119
+ export function splitActionInRow(
120
+ data: TimelineRow[],
121
+ rowId: string,
122
+ actionId: string,
123
+ cutTime: number
124
+ ): TimelineRow[] {
125
+ const rowIdx = data.findIndex((r) => r.id === rowId);
126
+ if (rowIdx === -1) return data;
127
+
128
+ const row = data[rowIdx];
129
+ const actIdx = row.actions.findIndex((a) => a.id === actionId);
130
+ if (actIdx === -1) return data;
131
+
132
+ const action = row.actions[actIdx];
133
+
134
+ // Validate the cut time is within the block boundaries
135
+ if (cutTime <= action.start || cutTime >= action.end) {
136
+ return data;
137
+ }
138
+
139
+ // Clone immutably
140
+ const newData = [...data];
141
+ const targetRow = { ...row, actions: [...row.actions] };
142
+ newData[rowIdx] = targetRow;
143
+
144
+ const targetAction = { ...action };
145
+ const originalEnd = targetAction.end;
146
+ targetAction.end = cutTime;
147
+ targetRow.actions[actIdx] = targetAction;
148
+
149
+ const newAction = {
150
+ ...targetAction,
151
+ id: `${targetAction.id}_split_${Date.now()}`,
152
+ start: cutTime,
153
+ end: originalEnd,
154
+ };
155
+
156
+ targetRow.actions.splice(actIdx + 1, 0, newAction);
157
+
158
+ return newData;
159
+ }