@twick/video-editor 0.15.14 → 0.15.15

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 (31) hide show
  1. package/dist/components/controls/control-manager.d.ts +2 -1
  2. package/dist/components/controls/player-controls.d.ts +13 -3
  3. package/dist/components/controls/seek-control.d.ts +3 -1
  4. package/dist/components/player/canvas-context-menu.d.ts +18 -0
  5. package/dist/components/player/player-manager.d.ts +5 -1
  6. package/dist/components/timeline/marquee-overlay.d.ts +12 -0
  7. package/dist/components/timeline/timeline-view.d.ts +21 -3
  8. package/dist/components/track/seek-track.d.ts +7 -1
  9. package/dist/components/track/track-base.d.ts +3 -2
  10. package/dist/components/track/track-element.d.ts +2 -1
  11. package/dist/components/track/track-header.d.ts +3 -3
  12. package/dist/components/video-editor.d.ts +6 -1
  13. package/dist/helpers/asset-type.d.ts +6 -0
  14. package/dist/helpers/constants.d.ts +12 -0
  15. package/dist/helpers/snap-targets.d.ts +7 -0
  16. package/dist/helpers/types.d.ts +10 -0
  17. package/dist/hooks/use-canvas-drop.d.ts +25 -0
  18. package/dist/hooks/use-canvas-keyboard.d.ts +13 -0
  19. package/dist/hooks/use-marquee-selection.d.ts +24 -0
  20. package/dist/hooks/use-player-manager.d.ts +14 -2
  21. package/dist/hooks/use-playhead-scroll.d.ts +19 -0
  22. package/dist/hooks/use-timeline-control.d.ts +1 -1
  23. package/dist/hooks/use-timeline-drop.d.ts +40 -0
  24. package/dist/hooks/use-timeline-selection.d.ts +13 -0
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +1961 -628
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +1963 -630
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/video-editor.css +180 -31
  31. package/package.json +5 -5
package/dist/index.mjs CHANGED
@@ -3,8 +3,8 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsxs, jsx } from "react/jsx-runtime";
5
5
  import { useLivePlayerContext, PLAYER_STATE, LivePlayer } from "@twick/live-player";
6
- import { useTimelineContext, TIMELINE_ACTION, getCurrentElements, CaptionElement as CaptionElement$1, ElementDeserializer, Track, getDecimalNumber, VideoElement as VideoElement$1, AudioElement, TrackElement, ValidationError, VALIDATION_ERROR_CODE } from "@twick/timeline";
7
- import React, { useState, useRef, useEffect, useMemo, forwardRef, createElement, createContext, useContext, useId, useCallback, useLayoutEffect, useInsertionEffect, Fragment, Component } from "react";
6
+ import { ImageElement as ImageElement$1, AudioElement, VideoElement as VideoElement$1, useTimelineContext, TIMELINE_ACTION, ElementDeserializer, getCurrentElements, CaptionElement as CaptionElement$1, getDecimalNumber, resolveIds, TrackElement, resolveId, Track, getElementIdsInRange, ValidationError, VALIDATION_ERROR_CODE, formatTimeWithFrames } from "@twick/timeline";
7
+ import React, { useState, useRef, useCallback, useEffect, useMemo, forwardRef, createElement, createContext, useContext, useId, useLayoutEffect, useInsertionEffect, Fragment, Component } from "react";
8
8
  function t(t2, e3, s2) {
9
9
  return (e3 = function(t3) {
10
10
  var e4 = function(t4, e5) {
@@ -6882,7 +6882,11 @@ const CANVAS_OPERATIONS = {
6882
6882
  /** Caption properties have been updated */
6883
6883
  CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED",
6884
6884
  /** Watermark has been updated */
6885
- WATERMARK_UPDATED: "WATERMARK_UPDATED"
6885
+ WATERMARK_UPDATED: "WATERMARK_UPDATED",
6886
+ /** A new element was added via drop on canvas; payload is { element } */
6887
+ ADDED_NEW_ELEMENT: "ADDED_NEW_ELEMENT",
6888
+ /** Z-order changed (bring to front / send to back). Payload is { elementId, direction }. Timeline should reorder tracks. */
6889
+ Z_ORDER_CHANGED: "Z_ORDER_CHANGED"
6886
6890
  };
6887
6891
  const ELEMENT_TYPES = {
6888
6892
  /** Text element type */
@@ -6988,6 +6992,49 @@ const reorderElementsByZIndex = (canvas) => {
6988
6992
  objects.forEach((obj) => canvas.add(obj));
6989
6993
  canvas.renderAll();
6990
6994
  };
6995
+ const changeZOrder = (canvas, elementId, direction) => {
6996
+ var _a, _b;
6997
+ if (!canvas) return null;
6998
+ const objects = canvas.getObjects();
6999
+ const sorted = [...objects].sort((a2, b2) => (a2.zIndex || 0) - (b2.zIndex || 0));
7000
+ const idx = sorted.findIndex((obj2) => {
7001
+ var _a2;
7002
+ return ((_a2 = obj2.get) == null ? void 0 : _a2.call(obj2, "id")) === elementId;
7003
+ });
7004
+ if (idx < 0) return null;
7005
+ const minZ = ((_a = sorted[0]) == null ? void 0 : _a.zIndex) ?? 0;
7006
+ const maxZ = ((_b = sorted[sorted.length - 1]) == null ? void 0 : _b.zIndex) ?? 0;
7007
+ const obj = sorted[idx];
7008
+ if (direction === "front") {
7009
+ obj.set("zIndex", maxZ + 1);
7010
+ reorderElementsByZIndex(canvas);
7011
+ return maxZ + 1;
7012
+ }
7013
+ if (direction === "back") {
7014
+ obj.set("zIndex", minZ - 1);
7015
+ reorderElementsByZIndex(canvas);
7016
+ return minZ - 1;
7017
+ }
7018
+ if (direction === "forward" && idx < sorted.length - 1) {
7019
+ const next = sorted[idx + 1];
7020
+ const myZ = obj.zIndex ?? idx;
7021
+ const nextZ = next.zIndex ?? idx + 1;
7022
+ obj.set("zIndex", nextZ);
7023
+ next.set("zIndex", myZ);
7024
+ reorderElementsByZIndex(canvas);
7025
+ return nextZ;
7026
+ }
7027
+ if (direction === "backward" && idx > 0) {
7028
+ const prev = sorted[idx - 1];
7029
+ const myZ = obj.zIndex ?? idx;
7030
+ const prevZ = prev.zIndex ?? idx - 1;
7031
+ obj.set("zIndex", prevZ);
7032
+ prev.set("zIndex", myZ);
7033
+ reorderElementsByZIndex(canvas);
7034
+ return prevZ;
7035
+ }
7036
+ return obj.zIndex ?? idx;
7037
+ };
6991
7038
  const getCanvasContext = (canvas) => {
6992
7039
  var _a, _b, _c, _d;
6993
7040
  if (!canvas || !((_b = (_a = canvas.elements) == null ? void 0 : _a.lower) == null ? void 0 : _b.ctx)) return;
@@ -7008,12 +7055,31 @@ const convertToCanvasPosition = (x2, y2, canvasMetadata) => {
7008
7055
  y: y2 * canvasMetadata.scaleY + canvasMetadata.height / 2
7009
7056
  };
7010
7057
  };
7058
+ const getObjectCanvasCenter = (obj) => {
7059
+ if (obj.getCenterPoint) {
7060
+ const p2 = obj.getCenterPoint();
7061
+ return { x: p2.x, y: p2.y };
7062
+ }
7063
+ return { x: obj.left ?? 0, y: obj.top ?? 0 };
7064
+ };
7065
+ const getObjectCanvasAngle = (obj) => {
7066
+ if (typeof obj.getTotalAngle === "function") {
7067
+ return obj.getTotalAngle();
7068
+ }
7069
+ return obj.angle ?? 0;
7070
+ };
7011
7071
  const convertToVideoPosition = (x2, y2, canvasMetadata, videoSize) => {
7012
7072
  return {
7013
7073
  x: Number((x2 / canvasMetadata.scaleX - videoSize.width / 2).toFixed(2)),
7014
7074
  y: Number((y2 / canvasMetadata.scaleY - videoSize.height / 2).toFixed(2))
7015
7075
  };
7016
7076
  };
7077
+ const convertToVideoDimensions = (widthCanvas, heightCanvas, canvasMetadata) => {
7078
+ return {
7079
+ width: Number((widthCanvas / canvasMetadata.scaleX).toFixed(2)),
7080
+ height: Number((heightCanvas / canvasMetadata.scaleY).toFixed(2))
7081
+ };
7082
+ };
7017
7083
  const getCurrentFrameEffect = (item, seekTime) => {
7018
7084
  var _a;
7019
7085
  let currentFrameEffect;
@@ -7547,7 +7613,8 @@ const setImageProps = ({
7547
7613
  img,
7548
7614
  element,
7549
7615
  index,
7550
- canvasMetadata
7616
+ canvasMetadata,
7617
+ lockAspectRatio = true
7551
7618
  }) => {
7552
7619
  var _a, _b, _c, _d, _e2;
7553
7620
  const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
@@ -7567,13 +7634,15 @@ const setImageProps = ({
7567
7634
  img.set("selectable", true);
7568
7635
  img.set("hasControls", true);
7569
7636
  img.set("touchAction", "all");
7637
+ img.set("lockUniScaling", lockAspectRatio);
7570
7638
  };
7571
7639
  const addCaptionElement = ({
7572
7640
  element,
7573
7641
  index,
7574
7642
  canvas,
7575
7643
  captionProps,
7576
- canvasMetadata
7644
+ canvasMetadata,
7645
+ lockAspectRatio = false
7577
7646
  }) => {
7578
7647
  var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2, _o2, _p, _q, _r2, _s2, _t2, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J;
7579
7648
  const { x: x2, y: y2 } = convertToCanvasPosition(
@@ -7612,6 +7681,7 @@ const addCaptionElement = ({
7612
7681
  });
7613
7682
  caption.set("id", element.id);
7614
7683
  caption.set("zIndex", index);
7684
+ caption.set("lockUniScaling", lockAspectRatio);
7615
7685
  caption.controls.mt = disabledControl;
7616
7686
  caption.controls.mb = disabledControl;
7617
7687
  caption.controls.ml = disabledControl;
@@ -7659,7 +7729,8 @@ const addImageElement = async ({
7659
7729
  index,
7660
7730
  canvas,
7661
7731
  canvasMetadata,
7662
- currentFrameEffect
7732
+ currentFrameEffect,
7733
+ lockAspectRatio = true
7663
7734
  }) => {
7664
7735
  try {
7665
7736
  const img = await oa.fromURL(imageUrl || element.props.src || "");
@@ -7668,7 +7739,7 @@ const addImageElement = async ({
7668
7739
  originY: "center",
7669
7740
  lockMovementX: false,
7670
7741
  lockMovementY: false,
7671
- lockUniScaling: true,
7742
+ lockUniScaling: lockAspectRatio,
7672
7743
  hasControls: false,
7673
7744
  selectable: false
7674
7745
  });
@@ -7679,10 +7750,11 @@ const addImageElement = async ({
7679
7750
  index,
7680
7751
  canvas,
7681
7752
  canvasMetadata,
7682
- currentFrameEffect
7753
+ currentFrameEffect,
7754
+ lockAspectRatio
7683
7755
  });
7684
7756
  } else {
7685
- setImageProps({ img, element, index, canvasMetadata });
7757
+ setImageProps({ img, element, index, canvasMetadata, lockAspectRatio });
7686
7758
  canvas.add(img);
7687
7759
  return img;
7688
7760
  }
@@ -7695,7 +7767,8 @@ const addMediaGroup = ({
7695
7767
  index,
7696
7768
  canvas,
7697
7769
  canvasMetadata,
7698
- currentFrameEffect
7770
+ currentFrameEffect,
7771
+ lockAspectRatio = true
7699
7772
  }) => {
7700
7773
  var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2;
7701
7774
  let frameSize;
@@ -7784,6 +7857,7 @@ const addMediaGroup = ({
7784
7857
  group.controls.mtr = rotateControl;
7785
7858
  group.set("id", element.id);
7786
7859
  group.set("zIndex", index);
7860
+ group.set("lockUniScaling", lockAspectRatio);
7787
7861
  canvas.add(group);
7788
7862
  return group;
7789
7863
  };
@@ -7791,7 +7865,8 @@ const addRectElement = ({
7791
7865
  element,
7792
7866
  index,
7793
7867
  canvas,
7794
- canvasMetadata
7868
+ canvasMetadata,
7869
+ lockAspectRatio = false
7795
7870
  }) => {
7796
7871
  var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k;
7797
7872
  const { x: x2, y: y2 } = convertToCanvasPosition(
@@ -7829,6 +7904,7 @@ const addRectElement = ({
7829
7904
  });
7830
7905
  rect.set("id", element.id);
7831
7906
  rect.set("zIndex", index);
7907
+ rect.set("lockUniScaling", lockAspectRatio);
7832
7908
  rect.controls.mtr = rotateControl;
7833
7909
  canvas.add(rect);
7834
7910
  return rect;
@@ -7837,9 +7913,10 @@ const addCircleElement = ({
7837
7913
  element,
7838
7914
  index,
7839
7915
  canvas,
7840
- canvasMetadata
7916
+ canvasMetadata,
7917
+ lockAspectRatio = true
7841
7918
  }) => {
7842
- var _a, _b, _c, _d, _e2, _f;
7919
+ var _a, _b, _c, _d, _e2, _f, _g;
7843
7920
  const { x: x2, y: y2 } = convertToCanvasPosition(
7844
7921
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
7845
7922
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
@@ -7855,7 +7932,9 @@ const addCircleElement = ({
7855
7932
  stroke: ((_e2 = element.props) == null ? void 0 : _e2.stroke) || "#000000",
7856
7933
  strokeWidth: (((_f = element.props) == null ? void 0 : _f.lineWidth) || 0) * canvasMetadata.scaleX,
7857
7934
  originX: "center",
7858
- originY: "center"
7935
+ originY: "center",
7936
+ // Respect element opacity (0–1). Defaults to fully opaque.
7937
+ opacity: ((_g = element.props) == null ? void 0 : _g.opacity) ?? 1
7859
7938
  });
7860
7939
  circle.controls.mt = disabledControl;
7861
7940
  circle.controls.mb = disabledControl;
@@ -7864,6 +7943,7 @@ const addCircleElement = ({
7864
7943
  circle.controls.mtr = disabledControl;
7865
7944
  circle.set("id", element.id);
7866
7945
  circle.set("zIndex", index);
7946
+ circle.set("lockUniScaling", lockAspectRatio);
7867
7947
  canvas.add(circle);
7868
7948
  return circle;
7869
7949
  };
@@ -7933,19 +8013,23 @@ const VideoElement = {
7933
8013
  }
7934
8014
  },
7935
8015
  updateFromFabricObject(object, element, context) {
8016
+ const canvasCenter = getObjectCanvasCenter(object);
7936
8017
  const { x: x2, y: y2 } = convertToVideoPosition(
7937
- object.left,
7938
- object.top,
8018
+ canvasCenter.x,
8019
+ canvasCenter.y,
7939
8020
  context.canvasMetadata,
7940
8021
  context.videoSize
7941
8022
  );
8023
+ const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
8024
+ const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
8025
+ const { width: fw, height: fh2 } = convertToVideoDimensions(
8026
+ scaledW,
8027
+ scaledH,
8028
+ context.canvasMetadata
8029
+ );
8030
+ const updatedFrameSize = [fw, fh2];
7942
8031
  const currentFrameEffect = context.elementFrameMapRef.current[element.id];
7943
- let updatedFrameSize;
7944
8032
  if (currentFrameEffect) {
7945
- updatedFrameSize = [
7946
- currentFrameEffect.props.frameSize[0] * object.scaleX,
7947
- currentFrameEffect.props.frameSize[1] * object.scaleY
7948
- ];
7949
8033
  context.elementFrameMapRef.current[element.id] = {
7950
8034
  ...currentFrameEffect,
7951
8035
  props: {
@@ -7971,16 +8055,12 @@ const VideoElement = {
7971
8055
  };
7972
8056
  }
7973
8057
  const frame2 = element.frame;
7974
- updatedFrameSize = [
7975
- (frame2.size[0] ?? 0) * object.scaleX,
7976
- (frame2.size[1] ?? 0) * object.scaleY
7977
- ];
7978
8058
  return {
7979
8059
  element: {
7980
8060
  ...element,
7981
8061
  frame: {
7982
8062
  ...frame2,
7983
- rotation: object.angle,
8063
+ rotation: getObjectCanvasAngle(object),
7984
8064
  size: updatedFrameSize,
7985
8065
  x: x2,
7986
8066
  y: y2
@@ -7992,12 +8072,14 @@ const VideoElement = {
7992
8072
  const ImageElement = {
7993
8073
  name: ELEMENT_TYPES.IMAGE,
7994
8074
  async add(params) {
7995
- const { element, index, canvas, canvasMetadata } = params;
8075
+ var _a;
8076
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
7996
8077
  await addImageElement({
7997
8078
  element,
7998
8079
  index,
7999
8080
  canvas,
8000
- canvasMetadata
8081
+ canvasMetadata,
8082
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8001
8083
  });
8002
8084
  if (element.timelineType === "scene") {
8003
8085
  await addBackgroundColor({
@@ -8009,21 +8091,24 @@ const ImageElement = {
8009
8091
  }
8010
8092
  },
8011
8093
  updateFromFabricObject(object, element, context) {
8012
- var _a, _b;
8094
+ const canvasCenter = getObjectCanvasCenter(object);
8013
8095
  const { x: x2, y: y2 } = convertToVideoPosition(
8014
- object.left,
8015
- object.top,
8096
+ canvasCenter.x,
8097
+ canvasCenter.y,
8016
8098
  context.canvasMetadata,
8017
8099
  context.videoSize
8018
8100
  );
8019
8101
  const currentFrameEffect = context.elementFrameMapRef.current[element.id];
8020
8102
  if (object.type === "group") {
8021
- let updatedFrameSize;
8103
+ const scaledW2 = (object.width ?? 0) * (object.scaleX ?? 1);
8104
+ const scaledH2 = (object.height ?? 0) * (object.scaleY ?? 1);
8105
+ const { width: fw, height: fh2 } = convertToVideoDimensions(
8106
+ scaledW2,
8107
+ scaledH2,
8108
+ context.canvasMetadata
8109
+ );
8110
+ const updatedFrameSize = [fw, fh2];
8022
8111
  if (currentFrameEffect) {
8023
- updatedFrameSize = [
8024
- currentFrameEffect.props.frameSize[0] * object.scaleX,
8025
- currentFrameEffect.props.frameSize[1] * object.scaleY
8026
- ];
8027
8112
  context.elementFrameMapRef.current[element.id] = {
8028
8113
  ...currentFrameEffect,
8029
8114
  props: {
@@ -8035,6 +8120,15 @@ const ImageElement = {
8035
8120
  return {
8036
8121
  element: {
8037
8122
  ...element,
8123
+ // Keep the base frame in sync with the active frame effect
8124
+ // so visualizer `Rect {...element.frame}` reflects the same size/position.
8125
+ frame: element.frame ? {
8126
+ ...element.frame,
8127
+ rotation: getObjectCanvasAngle(object),
8128
+ size: updatedFrameSize,
8129
+ x: x2,
8130
+ y: y2
8131
+ } : element.frame,
8038
8132
  frameEffects: (element.frameEffects || []).map(
8039
8133
  (fe2) => fe2.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
8040
8134
  ...fe2,
@@ -8049,16 +8143,12 @@ const ImageElement = {
8049
8143
  };
8050
8144
  }
8051
8145
  const frame2 = element.frame;
8052
- updatedFrameSize = [
8053
- (frame2.size[0] ?? 0) * object.scaleX,
8054
- (frame2.size[1] ?? 0) * object.scaleY
8055
- ];
8056
8146
  return {
8057
8147
  element: {
8058
8148
  ...element,
8059
8149
  frame: {
8060
8150
  ...frame2,
8061
- rotation: object.angle,
8151
+ rotation: getObjectCanvasAngle(object),
8062
8152
  size: updatedFrameSize,
8063
8153
  x: x2,
8064
8154
  y: y2
@@ -8066,14 +8156,21 @@ const ImageElement = {
8066
8156
  }
8067
8157
  };
8068
8158
  }
8159
+ const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
8160
+ const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
8161
+ const { width, height } = convertToVideoDimensions(
8162
+ scaledW,
8163
+ scaledH,
8164
+ context.canvasMetadata
8165
+ );
8069
8166
  return {
8070
8167
  element: {
8071
8168
  ...element,
8072
8169
  props: {
8073
8170
  ...element.props,
8074
- rotation: object.angle,
8075
- width: (((_a = element.props) == null ? void 0 : _a.width) ?? 0) * object.scaleX,
8076
- height: (((_b = element.props) == null ? void 0 : _b.height) ?? 0) * object.scaleY,
8171
+ rotation: getObjectCanvasAngle(object),
8172
+ width,
8173
+ height,
8077
8174
  x: x2,
8078
8175
  y: y2
8079
8176
  }
@@ -8084,19 +8181,22 @@ const ImageElement = {
8084
8181
  const RectElement = {
8085
8182
  name: ELEMENT_TYPES.RECT,
8086
8183
  async add(params) {
8087
- const { element, index, canvas, canvasMetadata } = params;
8184
+ var _a;
8185
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
8088
8186
  await addRectElement({
8089
8187
  element,
8090
8188
  index,
8091
8189
  canvas,
8092
- canvasMetadata
8190
+ canvasMetadata,
8191
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8093
8192
  });
8094
8193
  },
8095
8194
  updateFromFabricObject(object, element, context) {
8096
8195
  var _a, _b;
8196
+ const canvasCenter = getObjectCanvasCenter(object);
8097
8197
  const { x: x2, y: y2 } = convertToVideoPosition(
8098
- object.left,
8099
- object.top,
8198
+ canvasCenter.x,
8199
+ canvasCenter.y,
8100
8200
  context.canvasMetadata,
8101
8201
  context.videoSize
8102
8202
  );
@@ -8105,7 +8205,7 @@ const RectElement = {
8105
8205
  ...element,
8106
8206
  props: {
8107
8207
  ...element.props,
8108
- rotation: object.angle,
8208
+ rotation: getObjectCanvasAngle(object),
8109
8209
  width: (((_a = element.props) == null ? void 0 : _a.width) ?? 0) * object.scaleX,
8110
8210
  height: (((_b = element.props) == null ? void 0 : _b.height) ?? 0) * object.scaleY,
8111
8211
  x: x2,
@@ -8118,36 +8218,41 @@ const RectElement = {
8118
8218
  const CircleElement = {
8119
8219
  name: ELEMENT_TYPES.CIRCLE,
8120
8220
  async add(params) {
8121
- const { element, index, canvas, canvasMetadata } = params;
8221
+ var _a;
8222
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
8122
8223
  await addCircleElement({
8123
8224
  element,
8124
8225
  index,
8125
8226
  canvas,
8126
- canvasMetadata
8227
+ canvasMetadata,
8228
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8127
8229
  });
8128
8230
  },
8129
8231
  updateFromFabricObject(object, element, context) {
8130
- var _a;
8232
+ var _a, _b;
8233
+ const canvasCenter = getObjectCanvasCenter(object);
8131
8234
  const { x: x2, y: y2 } = convertToVideoPosition(
8132
- object.left,
8133
- object.top,
8235
+ canvasCenter.x,
8236
+ canvasCenter.y,
8134
8237
  context.canvasMetadata,
8135
8238
  context.videoSize
8136
8239
  );
8137
8240
  const radius = Number(
8138
8241
  ((((_a = element.props) == null ? void 0 : _a.radius) ?? 0) * object.scaleX).toFixed(2)
8139
8242
  );
8243
+ const opacity = object.opacity != null ? object.opacity : (_b = element.props) == null ? void 0 : _b.opacity;
8140
8244
  return {
8141
8245
  element: {
8142
8246
  ...element,
8143
8247
  props: {
8144
8248
  ...element.props,
8145
- rotation: object.angle,
8249
+ rotation: getObjectCanvasAngle(object),
8146
8250
  radius,
8147
8251
  height: radius * 2,
8148
8252
  width: radius * 2,
8149
8253
  x: x2,
8150
- y: y2
8254
+ y: y2,
8255
+ ...opacity != null && { opacity }
8151
8256
  }
8152
8257
  }
8153
8258
  };
@@ -8165,9 +8270,10 @@ const TextElement = {
8165
8270
  });
8166
8271
  },
8167
8272
  updateFromFabricObject(object, element, context) {
8273
+ const canvasCenter = getObjectCanvasCenter(object);
8168
8274
  const { x: x2, y: y2 } = convertToVideoPosition(
8169
- object.left,
8170
- object.top,
8275
+ canvasCenter.x,
8276
+ canvasCenter.y,
8171
8277
  context.canvasMetadata,
8172
8278
  context.videoSize
8173
8279
  );
@@ -8176,7 +8282,7 @@ const TextElement = {
8176
8282
  ...element,
8177
8283
  props: {
8178
8284
  ...element.props,
8179
- rotation: object.angle,
8285
+ rotation: getObjectCanvasAngle(object),
8180
8286
  x: x2,
8181
8287
  y: y2
8182
8288
  }
@@ -8187,20 +8293,23 @@ const TextElement = {
8187
8293
  const CaptionElement = {
8188
8294
  name: ELEMENT_TYPES.CAPTION,
8189
8295
  async add(params) {
8190
- const { element, index, canvas, captionProps, canvasMetadata } = params;
8296
+ var _a;
8297
+ const { element, index, canvas, captionProps, canvasMetadata, lockAspectRatio } = params;
8191
8298
  await addCaptionElement({
8192
8299
  element,
8193
8300
  index,
8194
8301
  canvas,
8195
8302
  captionProps: captionProps ?? {},
8196
- canvasMetadata
8303
+ canvasMetadata,
8304
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8197
8305
  });
8198
8306
  },
8199
8307
  updateFromFabricObject(object, element, context) {
8200
8308
  var _a;
8309
+ const canvasCenter = getObjectCanvasCenter(object);
8201
8310
  const { x: x2, y: y2 } = convertToVideoPosition(
8202
- object.left,
8203
- object.top,
8311
+ canvasCenter.x,
8312
+ canvasCenter.y,
8204
8313
  context.canvasMetadata,
8205
8314
  context.videoSize
8206
8315
  );
@@ -8306,7 +8415,15 @@ function registerElements() {
8306
8415
  registerElements();
8307
8416
  const useTwickCanvas = ({
8308
8417
  onCanvasReady,
8309
- onCanvasOperation
8418
+ onCanvasOperation,
8419
+ /**
8420
+ * When true, holding Shift while dragging an object will lock movement to
8421
+ * the dominant axis (horizontal or vertical). This mirrors behavior in
8422
+ * professional editors and improves precise alignment.
8423
+ *
8424
+ * Default: false (opt‑in to avoid surprising existing consumers).
8425
+ */
8426
+ enableShiftAxisLock = false
8310
8427
  }) => {
8311
8428
  const [twickCanvas, setTwickCanvas] = useState(null);
8312
8429
  const elementMap = useRef({});
@@ -8316,6 +8433,7 @@ const useTwickCanvas = ({
8316
8433
  const videoSizeRef = useRef({ width: 1, height: 1 });
8317
8434
  const canvasResolutionRef = useRef({ width: 1, height: 1 });
8318
8435
  const captionPropsRef = useRef(null);
8436
+ const axisLockStateRef = useRef(null);
8319
8437
  const canvasMetadataRef = useRef({
8320
8438
  width: 0,
8321
8439
  height: 0,
@@ -8330,6 +8448,57 @@ const useTwickCanvas = ({
8330
8448
  canvasMetadataRef.current.scaleY = canvasMetadataRef.current.height / videoSize.height;
8331
8449
  }
8332
8450
  };
8451
+ const handleObjectMoving = (event) => {
8452
+ var _a;
8453
+ if (!enableShiftAxisLock) return;
8454
+ const target = event == null ? void 0 : event.target;
8455
+ const transform = event == null ? void 0 : event.transform;
8456
+ const pointerEvent = event == null ? void 0 : event.e;
8457
+ if (!target || !transform || !pointerEvent) {
8458
+ axisLockStateRef.current = null;
8459
+ return;
8460
+ }
8461
+ if (!pointerEvent.shiftKey) {
8462
+ axisLockStateRef.current = null;
8463
+ return;
8464
+ }
8465
+ const original = transform.original;
8466
+ if (!original || typeof target.left !== "number" || typeof target.top !== "number") {
8467
+ axisLockStateRef.current = null;
8468
+ return;
8469
+ }
8470
+ if (!axisLockStateRef.current) {
8471
+ const dx = Math.abs(target.left - original.left);
8472
+ const dy = Math.abs(target.top - original.top);
8473
+ axisLockStateRef.current = {
8474
+ axis: dx >= dy ? "x" : "y"
8475
+ };
8476
+ }
8477
+ if (axisLockStateRef.current.axis === "x") {
8478
+ target.top = original.top;
8479
+ } else {
8480
+ target.left = original.left;
8481
+ }
8482
+ (_a = target.canvas) == null ? void 0 : _a.requestRenderAll();
8483
+ };
8484
+ const applyMarqueeSelectionControls = () => {
8485
+ const canvasInstance = twickCanvasRef.current;
8486
+ if (!canvasInstance) return;
8487
+ const activeObject = canvasInstance.getActiveObject();
8488
+ if (!activeObject) return;
8489
+ if (activeObject instanceof Qo) {
8490
+ activeObject.controls.mt = disabledControl;
8491
+ activeObject.controls.mb = disabledControl;
8492
+ activeObject.controls.ml = disabledControl;
8493
+ activeObject.controls.mr = disabledControl;
8494
+ activeObject.controls.bl = disabledControl;
8495
+ activeObject.controls.br = disabledControl;
8496
+ activeObject.controls.tl = disabledControl;
8497
+ activeObject.controls.tr = disabledControl;
8498
+ activeObject.controls.mtr = rotateControl;
8499
+ canvasInstance.requestRenderAll();
8500
+ }
8501
+ };
8333
8502
  const buildCanvas = ({
8334
8503
  videoSize,
8335
8504
  canvasSize,
@@ -8349,6 +8518,9 @@ const useTwickCanvas = ({
8349
8518
  if (twickCanvasRef.current) {
8350
8519
  twickCanvasRef.current.off("mouse:up", handleMouseUp);
8351
8520
  twickCanvasRef.current.off("text:editing:exited", onTextEdit);
8521
+ twickCanvasRef.current.off("object:moving", handleObjectMoving);
8522
+ twickCanvasRef.current.off("selection:created", applyMarqueeSelectionControls);
8523
+ twickCanvasRef.current.off("selection:updated", applyMarqueeSelectionControls);
8352
8524
  twickCanvasRef.current.dispose();
8353
8525
  }
8354
8526
  const { canvas, canvasMetadata } = createCanvas({
@@ -8366,6 +8538,9 @@ const useTwickCanvas = ({
8366
8538
  videoSizeRef.current = videoSize;
8367
8539
  canvas == null ? void 0 : canvas.on("mouse:up", handleMouseUp);
8368
8540
  canvas == null ? void 0 : canvas.on("text:editing:exited", onTextEdit);
8541
+ canvas == null ? void 0 : canvas.on("object:moving", handleObjectMoving);
8542
+ canvas == null ? void 0 : canvas.on("selection:created", applyMarqueeSelectionControls);
8543
+ canvas == null ? void 0 : canvas.on("selection:updated", applyMarqueeSelectionControls);
8369
8544
  canvasResolutionRef.current = canvasSize;
8370
8545
  setTwickCanvas(canvas);
8371
8546
  twickCanvasRef.current = canvas;
@@ -8392,7 +8567,8 @@ const useTwickCanvas = ({
8392
8567
  if (event.target) {
8393
8568
  const object = event.target;
8394
8569
  const elementId = object.get("id");
8395
- if (((_a = event.transform) == null ? void 0 : _a.action) === "drag") {
8570
+ const action = (_a = event.transform) == null ? void 0 : _a.action;
8571
+ if (action === "drag") {
8396
8572
  const original = event.transform.original;
8397
8573
  if (object.left === original.left && object.top === original.top) {
8398
8574
  onCanvasOperation == null ? void 0 : onCanvasOperation(
@@ -8402,7 +8578,38 @@ const useTwickCanvas = ({
8402
8578
  return;
8403
8579
  }
8404
8580
  }
8405
- switch ((_b = event.transform) == null ? void 0 : _b.action) {
8581
+ const context = {
8582
+ canvasMetadata: canvasMetadataRef.current,
8583
+ videoSize: videoSizeRef.current,
8584
+ elementFrameMapRef: elementFrameMap,
8585
+ captionPropsRef,
8586
+ watermarkPropsRef
8587
+ };
8588
+ if (object instanceof Qo && (action === "drag" || action === "rotate")) {
8589
+ const objects = object.getObjects();
8590
+ for (const fabricObj of objects) {
8591
+ const id2 = fabricObj.get("id");
8592
+ if (!id2 || id2 === "e-watermark") continue;
8593
+ const currentElement = elementMap.current[id2];
8594
+ if (!currentElement) continue;
8595
+ const handler = elementController.get(currentElement.type);
8596
+ const result = (_b = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _b.call(
8597
+ handler,
8598
+ fabricObj,
8599
+ currentElement,
8600
+ context
8601
+ );
8602
+ if (result) {
8603
+ elementMap.current[id2] = result.element;
8604
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
8605
+ result.operation ?? CANVAS_OPERATIONS.ITEM_UPDATED,
8606
+ result.payload ?? result.element
8607
+ );
8608
+ }
8609
+ }
8610
+ return;
8611
+ }
8612
+ switch (action) {
8406
8613
  case "drag":
8407
8614
  case "scale":
8408
8615
  case "scaleX":
@@ -8412,13 +8619,7 @@ const useTwickCanvas = ({
8412
8619
  const handler = elementController.get(
8413
8620
  elementId === "e-watermark" ? "watermark" : currentElement == null ? void 0 : currentElement.type
8414
8621
  );
8415
- const result = (_c = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _c.call(handler, object, currentElement ?? { id: elementId, type: "text", props: {} }, {
8416
- canvasMetadata: canvasMetadataRef.current,
8417
- videoSize: videoSizeRef.current,
8418
- elementFrameMapRef: elementFrameMap,
8419
- captionPropsRef,
8420
- watermarkPropsRef
8421
- });
8622
+ const result = (_c = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _c.call(handler, object, currentElement ?? { id: elementId, type: "text", props: {} }, context);
8422
8623
  if (result) {
8423
8624
  elementMap.current[elementId] = result.element;
8424
8625
  onCanvasOperation == null ? void 0 : onCanvasOperation(
@@ -8436,7 +8637,8 @@ const useTwickCanvas = ({
8436
8637
  watermark,
8437
8638
  seekTime = 0,
8438
8639
  captionProps,
8439
- cleanAndAdd = false
8640
+ cleanAndAdd = false,
8641
+ lockAspectRatio
8440
8642
  }) => {
8441
8643
  if (!twickCanvas || !getCanvasContext(twickCanvas)) return;
8442
8644
  try {
@@ -8449,16 +8651,26 @@ const useTwickCanvas = ({
8449
8651
  }
8450
8652
  }
8451
8653
  captionPropsRef.current = captionProps;
8654
+ const uniqueElements = [];
8655
+ const seenIds = /* @__PURE__ */ new Set();
8656
+ for (const el of elements) {
8657
+ if (!el || !el.id) continue;
8658
+ if (seenIds.has(el.id)) continue;
8659
+ seenIds.add(el.id);
8660
+ uniqueElements.push(el);
8661
+ }
8452
8662
  await Promise.all(
8453
- elements.map(async (element, index) => {
8663
+ uniqueElements.map(async (element, index) => {
8454
8664
  try {
8455
8665
  if (!element) return;
8666
+ const zOrder = element.zIndex ?? index;
8456
8667
  await addElementToCanvas({
8457
8668
  element,
8458
- index,
8669
+ index: zOrder,
8459
8670
  reorder: false,
8460
8671
  seekTime,
8461
- captionProps
8672
+ captionProps,
8673
+ lockAspectRatio
8462
8674
  });
8463
8675
  } catch {
8464
8676
  }
@@ -8478,8 +8690,10 @@ const useTwickCanvas = ({
8478
8690
  index,
8479
8691
  reorder = true,
8480
8692
  seekTime,
8481
- captionProps
8693
+ captionProps,
8694
+ lockAspectRatio
8482
8695
  }) => {
8696
+ var _a;
8483
8697
  if (!twickCanvas) return;
8484
8698
  const handler = elementController.get(element.type);
8485
8699
  if (handler) {
@@ -8491,10 +8705,11 @@ const useTwickCanvas = ({
8491
8705
  seekTime,
8492
8706
  captionProps: captionProps ?? null,
8493
8707
  elementFrameMapRef: elementFrameMap,
8494
- getCurrentFrameEffect
8708
+ getCurrentFrameEffect,
8709
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8495
8710
  });
8496
8711
  }
8497
- elementMap.current[element.id] = element;
8712
+ elementMap.current[element.id] = { ...element, zIndex: element.zIndex ?? index };
8498
8713
  if (reorder) {
8499
8714
  reorderElementsByZIndex(twickCanvas);
8500
8715
  }
@@ -8515,169 +8730,868 @@ const useTwickCanvas = ({
8515
8730
  elementMap.current[element.id] = element;
8516
8731
  }
8517
8732
  };
8733
+ const applyZOrder = (elementId, direction) => {
8734
+ if (!twickCanvas) return false;
8735
+ const newZIndex = changeZOrder(twickCanvas, elementId, direction);
8736
+ if (newZIndex == null) return false;
8737
+ const element = elementMap.current[elementId];
8738
+ if (element) elementMap.current[elementId] = { ...element, zIndex: newZIndex };
8739
+ onCanvasOperation == null ? void 0 : onCanvasOperation(CANVAS_OPERATIONS.Z_ORDER_CHANGED, { elementId, direction });
8740
+ return true;
8741
+ };
8742
+ const bringToFront = (elementId) => applyZOrder(elementId, "front");
8743
+ const sendToBack = (elementId) => applyZOrder(elementId, "back");
8744
+ const bringForward = (elementId) => applyZOrder(elementId, "forward");
8745
+ const sendBackward = (elementId) => applyZOrder(elementId, "backward");
8518
8746
  return {
8519
8747
  twickCanvas,
8520
8748
  buildCanvas,
8521
8749
  onVideoSizeChange,
8522
8750
  addWatermarkToCanvas,
8523
8751
  addElementToCanvas,
8524
- setCanvasElements
8752
+ setCanvasElements,
8753
+ bringToFront,
8754
+ sendToBack,
8755
+ bringForward,
8756
+ sendBackward
8525
8757
  };
8526
8758
  };
8527
- const usePlayerManager = ({
8528
- videoProps
8529
- }) => {
8530
- const [projectData, setProjectData] = useState(null);
8531
- const {
8532
- timelineAction,
8533
- setTimelineAction,
8534
- setSelectedItem,
8535
- editor,
8536
- changeLog
8537
- } = useTimelineContext();
8538
- const currentChangeLog = useRef(changeLog);
8539
- const prevSeekTime = useRef(0);
8540
- const [playerUpdating, setPlayerUpdating] = useState(false);
8541
- const handleCanvasReady = (_canvas) => {
8542
- };
8543
- const handleCanvasOperation = (operation, data) => {
8544
- if (operation === CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED) {
8545
- const subtitlesTrack = editor.getSubtitlesTrack();
8546
- subtitlesTrack == null ? void 0 : subtitlesTrack.setProps(data.props);
8547
- setSelectedItem(data.element);
8548
- editor.refresh();
8549
- } else if (operation === CANVAS_OPERATIONS.WATERMARK_UPDATED) {
8550
- const w2 = editor.getWatermark();
8551
- if (w2 && data) {
8552
- if (data.position) w2.setPosition(data.position);
8553
- if (data.rotation != null) w2.setRotation(data.rotation);
8554
- if (data.opacity != null) w2.setOpacity(data.opacity);
8555
- if (data.props) w2.setProps(data.props);
8556
- editor.setWatermark(w2);
8557
- currentChangeLog.current = currentChangeLog.current + 1;
8558
- }
8559
- } else {
8560
- const element = ElementDeserializer.fromJSON(data);
8561
- switch (operation) {
8562
- case CANVAS_OPERATIONS.ITEM_SELECTED:
8563
- setSelectedItem(element);
8564
- break;
8565
- case CANVAS_OPERATIONS.ITEM_UPDATED:
8566
- if (element) {
8567
- const updatedElement = editor.updateElement(element);
8568
- currentChangeLog.current = currentChangeLog.current + 1;
8569
- setSelectedItem(updatedElement);
8570
- }
8571
- break;
8572
- }
8573
- }
8574
- };
8575
- const { twickCanvas, buildCanvas, setCanvasElements } = useTwickCanvas({
8576
- onCanvasReady: handleCanvasReady,
8577
- onCanvasOperation: handleCanvasOperation
8578
- });
8579
- const updateCanvas = (seekTime) => {
8580
- var _a;
8581
- if (changeLog === currentChangeLog.current && seekTime === prevSeekTime.current) {
8582
- return;
8583
- }
8584
- prevSeekTime.current = seekTime;
8585
- const elements = getCurrentElements(
8586
- seekTime,
8587
- ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? []
8588
- );
8589
- let captionProps = {};
8590
- (elements || []).forEach((element) => {
8591
- if (element instanceof CaptionElement$1) {
8592
- const track = editor.getTrackById(element.getTrackId());
8593
- captionProps = (track == null ? void 0 : track.getProps()) ?? {};
8594
- }
8595
- });
8596
- const watermark = editor.getWatermark();
8597
- let watermarkElement;
8598
- if (watermark) {
8599
- const position = watermark.getPosition();
8600
- watermarkElement = {
8601
- id: watermark.getId(),
8602
- type: watermark.getType(),
8603
- props: {
8604
- ...watermark.getProps() || {},
8605
- x: (position == null ? void 0 : position.x) ?? 0,
8606
- y: (position == null ? void 0 : position.y) ?? 0,
8607
- rotation: watermark.getRotation() ?? 0,
8608
- opacity: watermark.getOpacity() ?? 1
8609
- }
8610
- };
8611
- }
8612
- setCanvasElements({
8613
- elements,
8614
- watermark: watermarkElement,
8615
- seekTime,
8616
- captionProps,
8617
- cleanAndAdd: true
8618
- });
8619
- currentChangeLog.current = changeLog;
8620
- };
8621
- const onPlayerUpdate = (event) => {
8622
- var _a;
8623
- if (((_a = event == null ? void 0 : event.detail) == null ? void 0 : _a.status) === "ready") {
8624
- setPlayerUpdating(false);
8625
- setTimelineAction(TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
8626
- }
8627
- };
8628
- useEffect(() => {
8629
- var _a, _b, _c, _d, _e2;
8630
- switch (timelineAction.type) {
8631
- case TIMELINE_ACTION.UPDATE_PLAYER_DATA:
8632
- if (videoProps) {
8633
- if (((_a = timelineAction.payload) == null ? void 0 : _a.forceUpdate) || editor.getLatestVersion() !== ((_b = projectData == null ? void 0 : projectData.input) == null ? void 0 : _b.version)) {
8634
- setPlayerUpdating(true);
8635
- const _latestProjectData = {
8636
- input: {
8637
- properties: videoProps,
8638
- tracks: ((_c = timelineAction.payload) == null ? void 0 : _c.tracks) ?? [],
8639
- version: ((_d = timelineAction.payload) == null ? void 0 : _d.version) ?? 0
8640
- }
8641
- };
8642
- setProjectData(_latestProjectData);
8643
- if (((_e2 = timelineAction.payload) == null ? void 0 : _e2.version) === 1) {
8644
- setTimeout(() => {
8645
- setPlayerUpdating(false);
8646
- });
8647
- }
8648
- } else {
8649
- setTimelineAction(TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
8759
+ const VIDEO_TYPES = [
8760
+ "video/mp4",
8761
+ "video/webm",
8762
+ "video/ogg",
8763
+ "video/quicktime",
8764
+ "video/x-msvideo",
8765
+ "video/x-matroska"
8766
+ ];
8767
+ const AUDIO_TYPES = [
8768
+ "audio/mpeg",
8769
+ "audio/mp3",
8770
+ "audio/wav",
8771
+ "audio/ogg",
8772
+ "audio/webm",
8773
+ "audio/aac",
8774
+ "audio/mp4",
8775
+ "audio/x-wav"
8776
+ ];
8777
+ const IMAGE_TYPES = [
8778
+ "image/jpeg",
8779
+ "image/jpg",
8780
+ "image/png",
8781
+ "image/gif",
8782
+ "image/webp",
8783
+ "image/svg+xml",
8784
+ "image/bmp"
8785
+ ];
8786
+ const EXT_TO_TYPE = {
8787
+ mp4: "video",
8788
+ webm: "video",
8789
+ mov: "video",
8790
+ avi: "video",
8791
+ mkv: "video",
8792
+ mp3: "audio",
8793
+ wav: "audio",
8794
+ ogg: "audio",
8795
+ m4a: "audio",
8796
+ jpg: "image",
8797
+ jpeg: "image",
8798
+ png: "image",
8799
+ gif: "image",
8800
+ webp: "image",
8801
+ svg: "image",
8802
+ bmp: "image"
8803
+ };
8804
+ function getAssetTypeFromFile(file) {
8805
+ var _a;
8806
+ const mime = (file.type || "").toLowerCase();
8807
+ const ext = (((_a = file.name) == null ? void 0 : _a.split(".").pop()) || "").toLowerCase();
8808
+ if (VIDEO_TYPES.some((t2) => mime.includes(t2))) return "video";
8809
+ if (AUDIO_TYPES.some((t2) => mime.includes(t2))) return "audio";
8810
+ if (IMAGE_TYPES.some((t2) => mime.includes(t2))) return "image";
8811
+ if (ext && EXT_TO_TYPE[ext]) return EXT_TO_TYPE[ext];
8812
+ return null;
8813
+ }
8814
+ const INITIAL_TIMELINE_DATA = {
8815
+ tracks: [
8816
+ {
8817
+ type: "element",
8818
+ id: "t-sample",
8819
+ name: "sample",
8820
+ elements: [
8821
+ {
8822
+ id: "e-sample",
8823
+ trackId: "t-sample",
8824
+ name: "sample",
8825
+ type: "text",
8826
+ s: 0,
8827
+ e: 5,
8828
+ props: {
8829
+ text: "Twick Video Editor",
8830
+ fill: "#FFFFFF"
8650
8831
  }
8651
8832
  }
8652
- break;
8833
+ ]
8653
8834
  }
8654
- }, [timelineAction]);
8655
- return {
8656
- twickCanvas,
8657
- projectData,
8658
- updateCanvas,
8659
- buildCanvas,
8660
- onPlayerUpdate,
8661
- playerUpdating
8662
- };
8835
+ ],
8836
+ version: 1
8663
8837
  };
8664
- const PlayerManager = ({
8665
- videoProps,
8666
- playerProps,
8667
- canvasMode
8668
- }) => {
8669
- const { changeLog } = useTimelineContext();
8670
- const { twickCanvas, projectData, updateCanvas, playerUpdating, onPlayerUpdate, buildCanvas } = usePlayerManager({ videoProps });
8671
- const durationRef = useRef(0);
8672
- const {
8673
- playerState,
8674
- playerVolume,
8675
- seekTime,
8838
+ const MIN_DURATION = 0.1;
8839
+ const TIMELINE_DROP_MEDIA_TYPE = "application/x-twick-media";
8840
+ const DRAG_TYPE = {
8841
+ /** Drag operation is starting */
8842
+ START: "start",
8843
+ /** Drag operation is in progress */
8844
+ MOVE: "move",
8845
+ /** Drag operation has ended */
8846
+ END: "end"
8847
+ };
8848
+ const DEFAULT_TIMELINE_ZOOM = 1.5;
8849
+ const DEFAULT_FPS = 30;
8850
+ const SNAP_THRESHOLD_PX = 10;
8851
+ const DEFAULT_TIMELINE_ZOOM_CONFIG = {
8852
+ /** Minimum zoom level (10%) */
8853
+ min: 0.1,
8854
+ /** Maximum zoom level (300%) */
8855
+ max: 3,
8856
+ /** Zoom step increment/decrement (10%) */
8857
+ step: 0.1,
8858
+ /** Default zoom level (150%) */
8859
+ default: 1.5
8860
+ };
8861
+ const DEFAULT_TIMELINE_TICK_CONFIGS = [
8862
+ {
8863
+ durationThreshold: 10,
8864
+ // < 10 seconds
8865
+ majorInterval: 1,
8866
+ // 1s major ticks
8867
+ minorTicks: 10
8868
+ // 0.1s minor ticks (10 minors between majors)
8869
+ },
8870
+ {
8871
+ durationThreshold: 30,
8872
+ // < 30 seconds
8873
+ majorInterval: 5,
8874
+ // 5s major ticks
8875
+ minorTicks: 5
8876
+ // 1s minor ticks (5 minors between majors)
8877
+ },
8878
+ {
8879
+ durationThreshold: 120,
8880
+ // < 2 minutes
8881
+ majorInterval: 10,
8882
+ // 10s major ticks
8883
+ minorTicks: 5
8884
+ // 2s minor ticks (5 minors between majors)
8885
+ },
8886
+ {
8887
+ durationThreshold: 300,
8888
+ // < 5 minutes
8889
+ majorInterval: 30,
8890
+ // 30s major ticks
8891
+ minorTicks: 6
8892
+ // 5s minor ticks (6 minors between majors)
8893
+ },
8894
+ {
8895
+ durationThreshold: 900,
8896
+ // < 15 minutes
8897
+ majorInterval: 60,
8898
+ // 1m major ticks
8899
+ minorTicks: 6
8900
+ // 10s minor ticks (6 minors between majors)
8901
+ },
8902
+ {
8903
+ durationThreshold: 1800,
8904
+ // < 30 minutes
8905
+ majorInterval: 120,
8906
+ // 2m major ticks
8907
+ minorTicks: 4
8908
+ // 30s minor ticks (4 minors between majors)
8909
+ },
8910
+ {
8911
+ durationThreshold: 3600,
8912
+ // < 1 hour
8913
+ majorInterval: 300,
8914
+ // 5m major ticks
8915
+ minorTicks: 5
8916
+ // 1m minor ticks (5 minors between majors)
8917
+ },
8918
+ {
8919
+ durationThreshold: 7200,
8920
+ // < 2 hours
8921
+ majorInterval: 600,
8922
+ // 10m major ticks
8923
+ minorTicks: 10
8924
+ // 1m minor ticks (10 minors between majors)
8925
+ },
8926
+ {
8927
+ durationThreshold: Infinity,
8928
+ // >= 2 hours
8929
+ majorInterval: 1800,
8930
+ // 30m major ticks
8931
+ minorTicks: 6
8932
+ // 5m minor ticks (6 minors between majors)
8933
+ }
8934
+ ];
8935
+ const DEFAULT_ELEMENT_COLORS = {
8936
+ /** Fragment element color - deep charcoal matching UI background */
8937
+ fragment: "#1A1A1A",
8938
+ /** Video element color - vibrant royal purple */
8939
+ video: "#8B5FBF",
8940
+ /** Caption element color - soft wisteria purple */
8941
+ caption: "#9B8ACE",
8942
+ /** Image element color - warm copper accent */
8943
+ image: "#D4956C",
8944
+ /** Audio element color - deep teal */
8945
+ audio: "#3D8B8B",
8946
+ /** Text element color - medium lavender */
8947
+ text: "#8D74C4",
8948
+ /** Generic element color - muted amethyst */
8949
+ element: "#7B68B8",
8950
+ /** Rectangle element color - deep indigo */
8951
+ rect: "#5B4B99",
8952
+ /** Frame effect color - rich magenta */
8953
+ frameEffect: "#B55B9C",
8954
+ /** Filters color - periwinkle blue */
8955
+ filters: "#7A89D4",
8956
+ /** Transition color - burnished bronze */
8957
+ transition: "#BE8157",
8958
+ /** Animation color - muted emerald */
8959
+ animation: "#4B9B78",
8960
+ /** Icon element color - bright orchid */
8961
+ icon: "#A76CD4",
8962
+ /** Circle element color - deep byzantium */
8963
+ circle: "#703D8B"
8964
+ };
8965
+ const AVAILABLE_TEXT_FONTS = {
8966
+ // Google Fonts
8967
+ /** Modern sans-serif font */
8968
+ RUBIK: "Rubik",
8969
+ /** Clean and readable font */
8970
+ MULISH: "Mulish",
8971
+ /** Bold display font */
8972
+ LUCKIEST_GUY: "Luckiest Guy",
8973
+ /** Elegant serif font */
8974
+ PLAYFAIR_DISPLAY: "Playfair Display",
8975
+ /** Classic sans-serif font */
8976
+ ROBOTO: "Roboto",
8977
+ /** Modern geometric font */
8978
+ POPPINS: "Poppins",
8979
+ // Display and Decorative Fonts
8980
+ /** Comic-style display font */
8981
+ BANGERS: "Bangers",
8982
+ /** Handwritten-style font */
8983
+ BIRTHSTONE: "Birthstone",
8984
+ /** Elegant script font */
8985
+ CORINTHIA: "Corinthia",
8986
+ /** Formal script font */
8987
+ IMPERIAL_SCRIPT: "Imperial Script",
8988
+ /** Bold outline font */
8989
+ KUMAR_ONE_OUTLINE: "Kumar One Outline",
8990
+ /** Light outline font */
8991
+ LONDRI_OUTLINE: "Londrina Outline",
8992
+ /** Casual script font */
8993
+ MARCK_SCRIPT: "Marck Script",
8994
+ /** Modern sans-serif font */
8995
+ MONTSERRAT: "Montserrat",
8996
+ /** Stylish display font */
8997
+ PATTAYA: "Pattaya",
8998
+ // CDN Fonts
8999
+ /** Unique display font */
9000
+ PERALTA: "Peralta",
9001
+ /** Bold impact font */
9002
+ IMPACT: "Impact",
9003
+ /** Handwritten-style font */
9004
+ LUMANOSIMO: "Lumanosimo",
9005
+ /** Custom display font */
9006
+ KAPAKANA: "Kapakana",
9007
+ /** Handwritten font */
9008
+ HANDYRUSH: "HandyRush",
9009
+ /** Decorative font */
9010
+ DASHER: "Dasher",
9011
+ /** Signature-style font */
9012
+ BRITTANY_SIGNATURE: "Brittany Signature"
9013
+ };
9014
+ const DEFAULT_DROP_DURATION = 5;
9015
+ function useTimelineDrop({
9016
+ containerRef,
9017
+ scrollContainerRef,
9018
+ tracks,
9019
+ duration,
9020
+ zoomLevel,
9021
+ labelWidth,
9022
+ trackHeight,
9023
+ /** Width of the track content area (timeline minus labels). Used for accurate time mapping. */
9024
+ trackContentWidth,
9025
+ onDrop,
9026
+ enabled = true
9027
+ }) {
9028
+ const [preview, setPreview] = useState(null);
9029
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
9030
+ const computePosition = useCallback(
9031
+ (clientX, clientY) => {
9032
+ var _a, _b;
9033
+ if (!containerRef.current) return null;
9034
+ const rect = containerRef.current.getBoundingClientRect();
9035
+ const scrollEl = (scrollContainerRef == null ? void 0 : scrollContainerRef.current) ?? containerRef.current;
9036
+ const scrollLeft = (scrollEl == null ? void 0 : scrollEl.scrollLeft) ?? 0;
9037
+ const viewportLeft = ((_b = (_a = scrollEl == null ? void 0 : scrollEl.getBoundingClientRect) == null ? void 0 : _a.call(scrollEl)) == null ? void 0 : _b.left) ?? rect.left;
9038
+ const contentX = clientX - viewportLeft + scrollLeft - labelWidth;
9039
+ const relY = clientY - rect.top;
9040
+ const rawTrackIndex = Math.floor(relY / trackHeight);
9041
+ const trackIndex = tracks.length === 0 ? 0 : Math.max(0, Math.min(tracks.length - 1, rawTrackIndex));
9042
+ const pixelsPerSecond = trackContentWidth != null && trackContentWidth > 0 ? trackContentWidth / duration : 100 * zoomLevel;
9043
+ const timeSec = Math.max(
9044
+ 0,
9045
+ Math.min(duration, contentX / pixelsPerSecond)
9046
+ );
9047
+ return { trackIndex, timeSec };
9048
+ },
9049
+ [
9050
+ containerRef,
9051
+ scrollContainerRef,
9052
+ tracks.length,
9053
+ labelWidth,
9054
+ trackHeight,
9055
+ zoomLevel,
9056
+ duration,
9057
+ trackContentWidth
9058
+ ]
9059
+ );
9060
+ const handleDragOver = useCallback(
9061
+ (e3) => {
9062
+ if (!enabled) return;
9063
+ e3.preventDefault();
9064
+ e3.stopPropagation();
9065
+ const hasFiles = e3.dataTransfer.types.includes("Files");
9066
+ const hasPanelMedia = e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE);
9067
+ if (!hasFiles && !hasPanelMedia) return;
9068
+ e3.dataTransfer.dropEffect = "copy";
9069
+ setIsDraggingOver(true);
9070
+ const pos = computePosition(e3.clientX, e3.clientY);
9071
+ if (pos) {
9072
+ const track = tracks[pos.trackIndex] ?? null;
9073
+ if (track || tracks.length === 0) {
9074
+ setPreview({
9075
+ trackIndex: pos.trackIndex,
9076
+ timeSec: pos.timeSec,
9077
+ widthPct: Math.min(
9078
+ 100,
9079
+ DEFAULT_DROP_DURATION / duration * 100
9080
+ )
9081
+ });
9082
+ }
9083
+ }
9084
+ },
9085
+ [enabled, computePosition, tracks, duration]
9086
+ );
9087
+ const handleDragLeave = useCallback((e3) => {
9088
+ if (!e3.currentTarget.contains(e3.relatedTarget)) {
9089
+ setIsDraggingOver(false);
9090
+ setPreview(null);
9091
+ }
9092
+ }, []);
9093
+ const handleDrop = useCallback(
9094
+ async (e3) => {
9095
+ if (!enabled) return;
9096
+ e3.preventDefault();
9097
+ e3.stopPropagation();
9098
+ setIsDraggingOver(false);
9099
+ setPreview(null);
9100
+ const pos = computePosition(e3.clientX, e3.clientY);
9101
+ if (!pos || pos.trackIndex < 0) return;
9102
+ const track = tracks[pos.trackIndex] ?? null;
9103
+ if (e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE)) {
9104
+ try {
9105
+ const data = JSON.parse(
9106
+ e3.dataTransfer.getData(TIMELINE_DROP_MEDIA_TYPE) || "{}"
9107
+ );
9108
+ if (data.type && data.url) {
9109
+ await onDrop({
9110
+ track,
9111
+ timeSec: pos.timeSec,
9112
+ type: data.type,
9113
+ url: data.url
9114
+ });
9115
+ }
9116
+ } catch {
9117
+ }
9118
+ return;
9119
+ }
9120
+ const files = Array.from(e3.dataTransfer.files || []);
9121
+ for (const file of files) {
9122
+ const type = getAssetTypeFromFile(file);
9123
+ if (!type) continue;
9124
+ const blobUrl = URL.createObjectURL(file);
9125
+ try {
9126
+ await onDrop({
9127
+ track,
9128
+ timeSec: pos.timeSec,
9129
+ type,
9130
+ url: blobUrl
9131
+ });
9132
+ } finally {
9133
+ URL.revokeObjectURL(blobUrl);
9134
+ }
9135
+ break;
9136
+ }
9137
+ },
9138
+ [enabled, computePosition, tracks, onDrop]
9139
+ );
9140
+ return { preview, isDraggingOver, handleDragOver, handleDragLeave, handleDrop };
9141
+ }
9142
+ function createElementFromDrop(type, blobUrl, parentSize) {
9143
+ switch (type) {
9144
+ case "video":
9145
+ return new VideoElement$1(blobUrl, parentSize);
9146
+ case "audio":
9147
+ return new AudioElement(blobUrl);
9148
+ case "image":
9149
+ return new ImageElement$1(blobUrl, parentSize);
9150
+ default:
9151
+ throw new Error(`Unknown asset type: ${type}`);
9152
+ }
9153
+ }
9154
+ const usePlayerManager = ({
9155
+ videoProps,
9156
+ canvasConfig
9157
+ }) => {
9158
+ const [projectData, setProjectData] = useState(null);
9159
+ const {
9160
+ timelineAction,
9161
+ setTimelineAction,
9162
+ setSelectedItem,
9163
+ editor,
9164
+ changeLog,
9165
+ videoResolution
9166
+ } = useTimelineContext();
9167
+ const { getCurrentTime } = useLivePlayerContext();
9168
+ const currentChangeLog = useRef(changeLog);
9169
+ const prevSeekTime = useRef(0);
9170
+ const [playerUpdating, setPlayerUpdating] = useState(false);
9171
+ const handleCanvasReady = (_canvas) => {
9172
+ };
9173
+ const handleCanvasOperation = (operation, data) => {
9174
+ var _a;
9175
+ if (operation === CANVAS_OPERATIONS.ADDED_NEW_ELEMENT) {
9176
+ if (data == null ? void 0 : data.element) {
9177
+ setSelectedItem(data.element);
9178
+ }
9179
+ return;
9180
+ }
9181
+ if (operation === CANVAS_OPERATIONS.Z_ORDER_CHANGED) {
9182
+ const { elementId, direction } = data ?? {};
9183
+ if (!elementId || !direction) return;
9184
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
9185
+ const trackIndex = tracks.findIndex(
9186
+ (t2) => t2.getElements().some((el) => el.getId() === elementId)
9187
+ );
9188
+ if (trackIndex < 0) return;
9189
+ const track = tracks[trackIndex];
9190
+ const reordered = [...tracks];
9191
+ if (direction === "front") {
9192
+ reordered.splice(trackIndex, 1);
9193
+ reordered.push(track);
9194
+ } else if (direction === "back") {
9195
+ reordered.splice(trackIndex, 1);
9196
+ reordered.unshift(track);
9197
+ } else if (direction === "forward" && trackIndex < reordered.length - 1) {
9198
+ [reordered[trackIndex], reordered[trackIndex + 1]] = [reordered[trackIndex + 1], reordered[trackIndex]];
9199
+ } else if (direction === "backward" && trackIndex > 0) {
9200
+ [reordered[trackIndex - 1], reordered[trackIndex]] = [reordered[trackIndex], reordered[trackIndex - 1]];
9201
+ } else {
9202
+ return;
9203
+ }
9204
+ editor.reorderTracks(reordered);
9205
+ currentChangeLog.current = currentChangeLog.current + 1;
9206
+ return;
9207
+ }
9208
+ if (operation === CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED) {
9209
+ const captionsTrack = editor.getCaptionsTrack();
9210
+ captionsTrack == null ? void 0 : captionsTrack.setProps(data.props);
9211
+ setSelectedItem(data.element);
9212
+ editor.refresh();
9213
+ } else if (operation === CANVAS_OPERATIONS.WATERMARK_UPDATED) {
9214
+ const w2 = editor.getWatermark();
9215
+ if (w2 && data) {
9216
+ if (data.position) w2.setPosition(data.position);
9217
+ if (data.rotation != null) w2.setRotation(data.rotation);
9218
+ if (data.opacity != null) w2.setOpacity(data.opacity);
9219
+ if (data.props) w2.setProps(data.props);
9220
+ editor.setWatermark(w2);
9221
+ currentChangeLog.current = currentChangeLog.current + 1;
9222
+ }
9223
+ } else {
9224
+ const element = ElementDeserializer.fromJSON(data);
9225
+ switch (operation) {
9226
+ case CANVAS_OPERATIONS.ITEM_SELECTED:
9227
+ setSelectedItem(element);
9228
+ break;
9229
+ case CANVAS_OPERATIONS.ITEM_UPDATED:
9230
+ if (element) {
9231
+ const updatedElement = editor.updateElement(element);
9232
+ currentChangeLog.current = currentChangeLog.current + 1;
9233
+ setSelectedItem(updatedElement);
9234
+ }
9235
+ break;
9236
+ }
9237
+ }
9238
+ };
9239
+ const handleDropOnCanvas = useCallback(
9240
+ async (payload) => {
9241
+ const { type, url, canvasX, canvasY } = payload;
9242
+ const element = createElementFromDrop(type, url, videoResolution);
9243
+ const currentTime = getCurrentTime();
9244
+ element.setStart(currentTime);
9245
+ const newTrack = editor.addTrack(`Track_${Date.now()}`);
9246
+ const result = await editor.addElementToTrack(newTrack, element);
9247
+ if (result) {
9248
+ setSelectedItem(element);
9249
+ currentChangeLog.current = currentChangeLog.current + 1;
9250
+ editor.refresh();
9251
+ handleCanvasOperation(CANVAS_OPERATIONS.ADDED_NEW_ELEMENT, {
9252
+ element,
9253
+ canvasPosition: canvasX != null && canvasY != null ? { x: canvasX, y: canvasY } : void 0
9254
+ });
9255
+ }
9256
+ },
9257
+ [editor, videoResolution, getCurrentTime, setSelectedItem]
9258
+ );
9259
+ const {
9260
+ twickCanvas,
9261
+ buildCanvas,
9262
+ setCanvasElements,
9263
+ bringToFront,
9264
+ sendToBack,
9265
+ bringForward,
9266
+ sendBackward
9267
+ } = useTwickCanvas({
9268
+ onCanvasReady: handleCanvasReady,
9269
+ onCanvasOperation: handleCanvasOperation,
9270
+ enableShiftAxisLock: (canvasConfig == null ? void 0 : canvasConfig.enableShiftAxisLock) ?? false
9271
+ });
9272
+ const updateCanvas = (seekTime) => {
9273
+ var _a;
9274
+ if (changeLog === currentChangeLog.current && seekTime === prevSeekTime.current) {
9275
+ return;
9276
+ }
9277
+ prevSeekTime.current = seekTime;
9278
+ const elements = getCurrentElements(
9279
+ seekTime,
9280
+ ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? []
9281
+ );
9282
+ let captionProps = {};
9283
+ (elements || []).forEach((element) => {
9284
+ if (element instanceof CaptionElement$1) {
9285
+ const track = editor.getTrackById(element.getTrackId());
9286
+ captionProps = (track == null ? void 0 : track.getProps()) ?? {};
9287
+ }
9288
+ });
9289
+ const watermark = editor.getWatermark();
9290
+ let watermarkElement;
9291
+ if (watermark) {
9292
+ const position = watermark.getPosition();
9293
+ watermarkElement = {
9294
+ id: watermark.getId(),
9295
+ type: watermark.getType(),
9296
+ props: {
9297
+ ...watermark.getProps() || {},
9298
+ x: (position == null ? void 0 : position.x) ?? 0,
9299
+ y: (position == null ? void 0 : position.y) ?? 0,
9300
+ rotation: watermark.getRotation() ?? 0,
9301
+ opacity: watermark.getOpacity() ?? 1
9302
+ }
9303
+ };
9304
+ }
9305
+ setCanvasElements({
9306
+ elements,
9307
+ watermark: watermarkElement,
9308
+ seekTime,
9309
+ captionProps,
9310
+ cleanAndAdd: true,
9311
+ lockAspectRatio: canvasConfig == null ? void 0 : canvasConfig.lockAspectRatio
9312
+ });
9313
+ currentChangeLog.current = changeLog;
9314
+ };
9315
+ const onPlayerUpdate = (event) => {
9316
+ var _a;
9317
+ if (((_a = event == null ? void 0 : event.detail) == null ? void 0 : _a.status) === "ready") {
9318
+ setPlayerUpdating(false);
9319
+ setTimelineAction(TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
9320
+ }
9321
+ };
9322
+ const deleteElement = (elementId) => {
9323
+ var _a;
9324
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
9325
+ for (const track of tracks) {
9326
+ const element = track.getElementById(elementId);
9327
+ if (element) {
9328
+ editor.removeElement(element);
9329
+ currentChangeLog.current = currentChangeLog.current + 1;
9330
+ setSelectedItem(null);
9331
+ updateCanvas(getCurrentTime());
9332
+ return;
9333
+ }
9334
+ }
9335
+ };
9336
+ useEffect(() => {
9337
+ var _a, _b, _c, _d, _e2;
9338
+ switch (timelineAction.type) {
9339
+ case TIMELINE_ACTION.UPDATE_PLAYER_DATA:
9340
+ if (videoProps) {
9341
+ if (((_a = timelineAction.payload) == null ? void 0 : _a.forceUpdate) || editor.getLatestVersion() !== ((_b = projectData == null ? void 0 : projectData.input) == null ? void 0 : _b.version)) {
9342
+ setPlayerUpdating(true);
9343
+ const _latestProjectData = {
9344
+ input: {
9345
+ properties: videoProps,
9346
+ tracks: ((_c = timelineAction.payload) == null ? void 0 : _c.tracks) ?? [],
9347
+ version: ((_d = timelineAction.payload) == null ? void 0 : _d.version) ?? 0
9348
+ }
9349
+ };
9350
+ setProjectData(_latestProjectData);
9351
+ if (((_e2 = timelineAction.payload) == null ? void 0 : _e2.version) === 1) {
9352
+ setTimeout(() => {
9353
+ setPlayerUpdating(false);
9354
+ });
9355
+ }
9356
+ } else {
9357
+ setTimelineAction(TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
9358
+ }
9359
+ }
9360
+ break;
9361
+ }
9362
+ }, [timelineAction]);
9363
+ return {
9364
+ twickCanvas,
9365
+ projectData,
9366
+ updateCanvas,
9367
+ buildCanvas,
9368
+ onPlayerUpdate,
9369
+ playerUpdating,
9370
+ handleDropOnCanvas,
9371
+ bringToFront,
9372
+ sendToBack,
9373
+ bringForward,
9374
+ sendBackward,
9375
+ deleteElement
9376
+ };
9377
+ };
9378
+ function useCanvasDrop({
9379
+ containerRef,
9380
+ videoSize,
9381
+ onDrop,
9382
+ enabled = true
9383
+ }) {
9384
+ const handleDragOver = useCallback(
9385
+ (e3) => {
9386
+ if (!enabled) return;
9387
+ e3.preventDefault();
9388
+ e3.stopPropagation();
9389
+ const hasFiles = e3.dataTransfer.types.includes("Files");
9390
+ const hasPanelMedia = e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE);
9391
+ if (!hasFiles && !hasPanelMedia) return;
9392
+ e3.dataTransfer.dropEffect = "copy";
9393
+ },
9394
+ [enabled]
9395
+ );
9396
+ const handleDrop = useCallback(
9397
+ async (e3) => {
9398
+ if (!enabled || !containerRef.current) return;
9399
+ e3.preventDefault();
9400
+ e3.stopPropagation();
9401
+ let type = null;
9402
+ let url = null;
9403
+ if (e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE)) {
9404
+ try {
9405
+ const data = JSON.parse(
9406
+ e3.dataTransfer.getData(TIMELINE_DROP_MEDIA_TYPE) || "{}"
9407
+ );
9408
+ if (data.type && data.url) {
9409
+ type = data.type;
9410
+ url = data.url;
9411
+ }
9412
+ } catch {
9413
+ }
9414
+ }
9415
+ if (!type || !url) {
9416
+ const files = Array.from(e3.dataTransfer.files || []);
9417
+ for (const file of files) {
9418
+ const detectedType = getAssetTypeFromFile(file);
9419
+ if (detectedType) {
9420
+ type = detectedType;
9421
+ url = URL.createObjectURL(file);
9422
+ try {
9423
+ await onDrop({
9424
+ type,
9425
+ url,
9426
+ canvasX: getCanvasX(e3, containerRef.current, videoSize),
9427
+ canvasY: getCanvasY(e3, containerRef.current, videoSize)
9428
+ });
9429
+ } finally {
9430
+ URL.revokeObjectURL(url);
9431
+ }
9432
+ return;
9433
+ }
9434
+ }
9435
+ return;
9436
+ }
9437
+ await onDrop({
9438
+ type,
9439
+ url,
9440
+ canvasX: getCanvasX(e3, containerRef.current, videoSize),
9441
+ canvasY: getCanvasY(e3, containerRef.current, videoSize)
9442
+ });
9443
+ },
9444
+ [enabled, containerRef, videoSize, onDrop]
9445
+ );
9446
+ const handleDragLeave = useCallback((e3) => {
9447
+ if (!e3.currentTarget.contains(e3.relatedTarget)) ;
9448
+ }, []);
9449
+ return { handleDragOver, handleDragLeave, handleDrop };
9450
+ }
9451
+ function getCanvasX(e3, container, videoSize) {
9452
+ const rect = container.getBoundingClientRect();
9453
+ const relX = (e3.clientX - rect.left) / rect.width;
9454
+ return Math.max(0, Math.min(videoSize.width, relX * videoSize.width));
9455
+ }
9456
+ function getCanvasY(e3, container, videoSize) {
9457
+ const rect = container.getBoundingClientRect();
9458
+ const relY = (e3.clientY - rect.top) / rect.height;
9459
+ return Math.max(0, Math.min(videoSize.height, relY * videoSize.height));
9460
+ }
9461
+ const CanvasContextMenu = ({
9462
+ x: x2,
9463
+ y: y2,
9464
+ elementId,
9465
+ onBringToFront,
9466
+ onSendToBack,
9467
+ onBringForward,
9468
+ onSendBackward,
9469
+ onDelete,
9470
+ onClose
9471
+ }) => {
9472
+ const menuRef = useRef(null);
9473
+ useEffect(() => {
9474
+ const handleClickOutside = (e3) => {
9475
+ if (menuRef.current && !menuRef.current.contains(e3.target)) {
9476
+ onClose();
9477
+ }
9478
+ };
9479
+ const handleEscape = (e3) => {
9480
+ if (e3.key === "Escape") onClose();
9481
+ };
9482
+ document.addEventListener("mousedown", handleClickOutside);
9483
+ document.addEventListener("keydown", handleEscape);
9484
+ return () => {
9485
+ document.removeEventListener("mousedown", handleClickOutside);
9486
+ document.removeEventListener("keydown", handleEscape);
9487
+ };
9488
+ }, [onClose]);
9489
+ const handleAction = (fn2) => {
9490
+ fn2(elementId);
9491
+ onClose();
9492
+ };
9493
+ return /* @__PURE__ */ jsxs(
9494
+ "div",
9495
+ {
9496
+ ref: menuRef,
9497
+ className: "twick-canvas-context-menu",
9498
+ style: { left: x2, top: y2 },
9499
+ role: "menu",
9500
+ children: [
9501
+ /* @__PURE__ */ jsx(
9502
+ "button",
9503
+ {
9504
+ type: "button",
9505
+ className: "twick-canvas-context-menu-item",
9506
+ onClick: () => handleAction(onBringToFront),
9507
+ role: "menuitem",
9508
+ children: "Bring to Front"
9509
+ }
9510
+ ),
9511
+ /* @__PURE__ */ jsx(
9512
+ "button",
9513
+ {
9514
+ type: "button",
9515
+ className: "twick-canvas-context-menu-item",
9516
+ onClick: () => handleAction(onBringForward),
9517
+ role: "menuitem",
9518
+ children: "Bring Forward"
9519
+ }
9520
+ ),
9521
+ /* @__PURE__ */ jsx(
9522
+ "button",
9523
+ {
9524
+ type: "button",
9525
+ className: "twick-canvas-context-menu-item",
9526
+ onClick: () => handleAction(onSendBackward),
9527
+ role: "menuitem",
9528
+ children: "Send Backward"
9529
+ }
9530
+ ),
9531
+ /* @__PURE__ */ jsx(
9532
+ "button",
9533
+ {
9534
+ type: "button",
9535
+ className: "twick-canvas-context-menu-item",
9536
+ onClick: () => handleAction(onSendToBack),
9537
+ role: "menuitem",
9538
+ children: "Send to Back"
9539
+ }
9540
+ ),
9541
+ /* @__PURE__ */ jsx("div", { className: "twick-canvas-context-menu-separator", role: "separator" }),
9542
+ /* @__PURE__ */ jsx(
9543
+ "button",
9544
+ {
9545
+ type: "button",
9546
+ className: "twick-canvas-context-menu-item twick-canvas-context-menu-item-danger",
9547
+ onClick: () => handleAction(onDelete),
9548
+ role: "menuitem",
9549
+ children: "Delete"
9550
+ }
9551
+ )
9552
+ ]
9553
+ }
9554
+ );
9555
+ };
9556
+ const PlayerManager = ({
9557
+ videoProps,
9558
+ playerProps,
9559
+ canvasMode,
9560
+ canvasConfig
9561
+ }) => {
9562
+ const containerRef = useRef(null);
9563
+ const canvasRef = useRef(null);
9564
+ const durationRef = useRef(0);
9565
+ const { changeLog } = useTimelineContext();
9566
+ const {
9567
+ playerState,
9568
+ playerVolume,
9569
+ seekTime,
8676
9570
  setPlayerState,
8677
9571
  setCurrentTime
8678
9572
  } = useLivePlayerContext();
8679
- const containerRef = useRef(null);
8680
- const canvasRef = useRef(null);
9573
+ const {
9574
+ twickCanvas,
9575
+ projectData,
9576
+ updateCanvas,
9577
+ playerUpdating,
9578
+ onPlayerUpdate,
9579
+ buildCanvas,
9580
+ handleDropOnCanvas,
9581
+ bringToFront,
9582
+ sendToBack,
9583
+ bringForward,
9584
+ sendBackward,
9585
+ deleteElement
9586
+ } = usePlayerManager({ videoProps, canvasConfig });
9587
+ const [contextMenu, setContextMenu] = useState(null);
9588
+ const closeContextMenu = useCallback(() => setContextMenu(null), []);
9589
+ const { handleDragOver, handleDragLeave, handleDrop } = useCanvasDrop({
9590
+ containerRef,
9591
+ videoSize: { width: videoProps.width, height: videoProps.height },
9592
+ onDrop: handleDropOnCanvas,
9593
+ enabled: !!handleDropOnCanvas && canvasMode
9594
+ });
8681
9595
  useEffect(() => {
8682
9596
  const container = containerRef.current;
8683
9597
  const canvasSize = {
@@ -8699,6 +9613,22 @@ const PlayerManager = ({
8699
9613
  updateCanvas(seekTime);
8700
9614
  }
8701
9615
  }, [twickCanvas, playerState, seekTime, changeLog]);
9616
+ useEffect(() => {
9617
+ if (!twickCanvas || !canvasMode) return;
9618
+ const onSelectionCreated = (e3) => {
9619
+ var _a, _b;
9620
+ const ev = e3 == null ? void 0 : e3.e;
9621
+ if (!ev) return;
9622
+ const id2 = (_b = (_a = e3.target) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "id");
9623
+ if (id2) {
9624
+ setContextMenu({ x: ev.clientX, y: ev.clientY, elementId: id2 });
9625
+ }
9626
+ };
9627
+ twickCanvas.on("contextmenu", onSelectionCreated);
9628
+ return () => {
9629
+ twickCanvas.off("contextmenu", onSelectionCreated);
9630
+ };
9631
+ }, [twickCanvas, canvasMode]);
8702
9632
  const handleTimeUpdate = (time2) => {
8703
9633
  if (durationRef.current && time2 >= durationRef.current) {
8704
9634
  setCurrentTime(0);
@@ -8755,8 +9685,26 @@ const PlayerManager = ({
8755
9685
  style: {
8756
9686
  opacity: playerState === PLAYER_STATE.PAUSED ? 1 : 0
8757
9687
  },
9688
+ onDragOver: handleDragOver,
9689
+ onDragLeave: handleDragLeave,
9690
+ onDrop: handleDrop,
9691
+ onContextMenu: (e3) => e3.preventDefault(),
8758
9692
  children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "twick-editor-canvas" })
8759
9693
  }
9694
+ ),
9695
+ contextMenu && /* @__PURE__ */ jsx(
9696
+ CanvasContextMenu,
9697
+ {
9698
+ x: contextMenu.x,
9699
+ y: contextMenu.y,
9700
+ elementId: contextMenu.elementId,
9701
+ onBringToFront: bringToFront,
9702
+ onSendToBack: sendToBack,
9703
+ onBringForward: bringForward,
9704
+ onSendBackward: sendBackward,
9705
+ onDelete: deleteElement,
9706
+ onClose: closeContextMenu
9707
+ }
8760
9708
  )
8761
9709
  ]
8762
9710
  }
@@ -10098,7 +11046,8 @@ function SeekTrack({
10098
11046
  zoom = 1,
10099
11047
  onSeek,
10100
11048
  timelineCount = 0,
10101
- timelineTickConfigs
11049
+ timelineTickConfigs,
11050
+ onPlayheadUpdate
10102
11051
  }) {
10103
11052
  const containerRef = useRef(null);
10104
11053
  const [isDragging2, setIsDragging] = useState(false);
@@ -10110,6 +11059,12 @@ function SeekTrack({
10110
11059
  const position = isDragging2 && dragPosition !== null ? dragPosition : currentTime * pixelsPerSecond;
10111
11060
  return Math.max(0, position);
10112
11061
  }, [isDragging2, dragPosition, currentTime, pixelsPerSecond]);
11062
+ React.useEffect(() => {
11063
+ onPlayheadUpdate == null ? void 0 : onPlayheadUpdate({
11064
+ positionPx: seekPosition,
11065
+ isDragging: isDragging2
11066
+ });
11067
+ }, [seekPosition, isDragging2, onPlayheadUpdate]);
10113
11068
  const { majorIntervalSec, minorIntervalSec } = useMemo(() => {
10114
11069
  if (timelineTickConfigs && timelineTickConfigs.length > 0) {
10115
11070
  const sortedConfigs = [...timelineTickConfigs].sort((a2, b2) => a2.durationThreshold - b2.durationThreshold);
@@ -10278,7 +11233,7 @@ function SeekTrack({
10278
11233
  transform: `translateX(${seekPosition}px)`,
10279
11234
  top: 0,
10280
11235
  touchAction: "none",
10281
- transition: isDragging2 ? "none" : "transform 0.1s linear",
11236
+ transition: isDragging2 ? "none" : "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)",
10282
11237
  willChange: isDragging2 ? "transform" : "auto"
10283
11238
  },
10284
11239
  children: [
@@ -10302,7 +11257,8 @@ const SeekControl = ({
10302
11257
  zoom,
10303
11258
  timelineCount,
10304
11259
  onSeek,
10305
- timelineTickConfigs
11260
+ timelineTickConfigs,
11261
+ onPlayheadUpdate
10306
11262
  }) => {
10307
11263
  const { currentTime } = useLivePlayerContext();
10308
11264
  return /* @__PURE__ */ jsx(
@@ -10313,7 +11269,8 @@ const SeekControl = ({
10313
11269
  zoom,
10314
11270
  onSeek,
10315
11271
  timelineCount,
10316
- timelineTickConfigs
11272
+ timelineTickConfigs,
11273
+ onPlayheadUpdate
10317
11274
  }
10318
11275
  );
10319
11276
  };
@@ -10422,7 +11379,21 @@ const createLucideIcon = (iconName, iconNode) => {
10422
11379
  * This source code is licensed under the ISC license.
10423
11380
  * See the LICENSE file in the root directory of this source tree.
10424
11381
  */
10425
- const __iconNode$b = [
11382
+ const __iconNode$e = [
11383
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
11384
+ ["line", { x1: "22", x2: "18", y1: "12", y2: "12", key: "l9bcsi" }],
11385
+ ["line", { x1: "6", x2: "2", y1: "12", y2: "12", key: "13hhkx" }],
11386
+ ["line", { x1: "12", x2: "12", y1: "6", y2: "2", key: "10w3f3" }],
11387
+ ["line", { x1: "12", x2: "12", y1: "22", y2: "18", key: "15g9kq" }]
11388
+ ];
11389
+ const Crosshair = createLucideIcon("crosshair", __iconNode$e);
11390
+ /**
11391
+ * @license lucide-react v0.511.0 - ISC
11392
+ *
11393
+ * This source code is licensed under the ISC license.
11394
+ * See the LICENSE file in the root directory of this source tree.
11395
+ */
11396
+ const __iconNode$d = [
10426
11397
  ["circle", { cx: "9", cy: "12", r: "1", key: "1vctgf" }],
10427
11398
  ["circle", { cx: "9", cy: "5", r: "1", key: "hp0tcf" }],
10428
11399
  ["circle", { cx: "9", cy: "19", r: "1", key: "fkjjf6" }],
@@ -10430,81 +11401,103 @@ const __iconNode$b = [
10430
11401
  ["circle", { cx: "15", cy: "5", r: "1", key: "19l28e" }],
10431
11402
  ["circle", { cx: "15", cy: "19", r: "1", key: "f4zoj3" }]
10432
11403
  ];
10433
- const GripVertical = createLucideIcon("grip-vertical", __iconNode$b);
11404
+ const GripVertical = createLucideIcon("grip-vertical", __iconNode$d);
10434
11405
  /**
10435
11406
  * @license lucide-react v0.511.0 - ISC
10436
11407
  *
10437
11408
  * This source code is licensed under the ISC license.
10438
11409
  * See the LICENSE file in the root directory of this source tree.
10439
11410
  */
10440
- const __iconNode$a = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
10441
- const LoaderCircle = createLucideIcon("loader-circle", __iconNode$a);
11411
+ const __iconNode$c = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
11412
+ const LoaderCircle = createLucideIcon("loader-circle", __iconNode$c);
10442
11413
  /**
10443
11414
  * @license lucide-react v0.511.0 - ISC
10444
11415
  *
10445
11416
  * This source code is licensed under the ISC license.
10446
11417
  * See the LICENSE file in the root directory of this source tree.
10447
11418
  */
10448
- const __iconNode$9 = [
11419
+ const __iconNode$b = [
10449
11420
  ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
10450
11421
  ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4", key: "fwvmzm" }]
10451
11422
  ];
10452
- const Lock = createLucideIcon("lock", __iconNode$9);
11423
+ const Lock = createLucideIcon("lock", __iconNode$b);
10453
11424
  /**
10454
11425
  * @license lucide-react v0.511.0 - ISC
10455
11426
  *
10456
11427
  * This source code is licensed under the ISC license.
10457
11428
  * See the LICENSE file in the root directory of this source tree.
10458
11429
  */
10459
- const __iconNode$8 = [
11430
+ const __iconNode$a = [
10460
11431
  ["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
10461
11432
  ["rect", { x: "6", y: "4", width: "4", height: "16", rx: "1", key: "1okwgv" }]
10462
11433
  ];
10463
- const Pause = createLucideIcon("pause", __iconNode$8);
11434
+ const Pause = createLucideIcon("pause", __iconNode$a);
10464
11435
  /**
10465
11436
  * @license lucide-react v0.511.0 - ISC
10466
11437
  *
10467
11438
  * This source code is licensed under the ISC license.
10468
11439
  * See the LICENSE file in the root directory of this source tree.
10469
11440
  */
10470
- const __iconNode$7 = [["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]];
10471
- const Play = createLucideIcon("play", __iconNode$7);
11441
+ const __iconNode$9 = [["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]];
11442
+ const Play = createLucideIcon("play", __iconNode$9);
10472
11443
  /**
10473
11444
  * @license lucide-react v0.511.0 - ISC
10474
11445
  *
10475
11446
  * This source code is licensed under the ISC license.
10476
11447
  * See the LICENSE file in the root directory of this source tree.
10477
11448
  */
10478
- const __iconNode$6 = [
11449
+ const __iconNode$8 = [
10479
11450
  ["path", { d: "M5 12h14", key: "1ays0h" }],
10480
11451
  ["path", { d: "M12 5v14", key: "s699le" }]
10481
11452
  ];
10482
- const Plus = createLucideIcon("plus", __iconNode$6);
11453
+ const Plus = createLucideIcon("plus", __iconNode$8);
10483
11454
  /**
10484
11455
  * @license lucide-react v0.511.0 - ISC
10485
11456
  *
10486
11457
  * This source code is licensed under the ISC license.
10487
11458
  * See the LICENSE file in the root directory of this source tree.
10488
11459
  */
10489
- const __iconNode$5 = [
11460
+ const __iconNode$7 = [
10490
11461
  ["path", { d: "m15 14 5-5-5-5", key: "12vg1m" }],
10491
11462
  ["path", { d: "M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13", key: "6uklza" }]
10492
11463
  ];
10493
- const Redo2 = createLucideIcon("redo-2", __iconNode$5);
11464
+ const Redo2 = createLucideIcon("redo-2", __iconNode$7);
10494
11465
  /**
10495
11466
  * @license lucide-react v0.511.0 - ISC
10496
11467
  *
10497
11468
  * This source code is licensed under the ISC license.
10498
11469
  * See the LICENSE file in the root directory of this source tree.
10499
11470
  */
10500
- const __iconNode$4 = [
11471
+ const __iconNode$6 = [
10501
11472
  ["circle", { cx: "6", cy: "6", r: "3", key: "1lh9wr" }],
10502
11473
  ["path", { d: "M8.12 8.12 12 12", key: "1alkpv" }],
10503
11474
  ["path", { d: "M20 4 8.12 15.88", key: "xgtan2" }],
10504
11475
  ["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
10505
11476
  ["path", { d: "M14.8 14.8 20 20", key: "ptml3r" }]
10506
11477
  ];
10507
- const Scissors = createLucideIcon("scissors", __iconNode$4);
11478
+ const Scissors = createLucideIcon("scissors", __iconNode$6);
11479
+ /**
11480
+ * @license lucide-react v0.511.0 - ISC
11481
+ *
11482
+ * This source code is licensed under the ISC license.
11483
+ * See the LICENSE file in the root directory of this source tree.
11484
+ */
11485
+ const __iconNode$5 = [
11486
+ ["polygon", { points: "19 20 9 12 19 4 19 20", key: "o2sva" }],
11487
+ ["line", { x1: "5", x2: "5", y1: "19", y2: "5", key: "1ocqjk" }]
11488
+ ];
11489
+ const SkipBack = createLucideIcon("skip-back", __iconNode$5);
11490
+ /**
11491
+ * @license lucide-react v0.511.0 - ISC
11492
+ *
11493
+ * This source code is licensed under the ISC license.
11494
+ * See the LICENSE file in the root directory of this source tree.
11495
+ */
11496
+ const __iconNode$4 = [
11497
+ ["polygon", { points: "5 4 15 12 5 20 5 4", key: "16p6eg" }],
11498
+ ["line", { x1: "19", x2: "19", y1: "5", y2: "19", key: "futhcm" }]
11499
+ ];
11500
+ const SkipForward = createLucideIcon("skip-forward", __iconNode$4);
10508
11501
  /**
10509
11502
  * @license lucide-react v0.511.0 - ISC
10510
11503
  *
@@ -10557,7 +11550,7 @@ const __iconNode = [
10557
11550
  const ZoomOut = createLucideIcon("zoom-out", __iconNode);
10558
11551
  const TrackHeader = ({
10559
11552
  track,
10560
- selectedItem,
11553
+ selectedIds,
10561
11554
  onDragStart,
10562
11555
  onDragOver,
10563
11556
  onDrop,
@@ -10566,9 +11559,9 @@ const TrackHeader = ({
10566
11559
  return /* @__PURE__ */ jsxs(
10567
11560
  "div",
10568
11561
  {
10569
- className: `twick-track-header ${selectedItem instanceof Track && selectedItem.getId() === track.getId() ? "twick-track-header-selected" : "twick-track-header-default"}`,
11562
+ className: `twick-track-header ${selectedIds.has(track.getId()) ? "twick-track-header-selected" : "twick-track-header-default"}`,
10570
11563
  draggable: true,
10571
- onClick: () => onSelect(track),
11564
+ onClick: (e3) => onSelect(track, e3),
10572
11565
  onDragStart: (e3) => onDragStart(e3, track),
10573
11566
  onDragOver,
10574
11567
  onDrop: (e3) => onDrop(e3, track),
@@ -17605,321 +18598,124 @@ class VisualElement {
17605
18598
  }
17606
18599
  const target = this.getBaseTargetFromProps(this.props, key);
17607
18600
  if (target !== void 0 && !isMotionValue(target))
17608
- return target;
17609
- return this.initialValues[key] !== void 0 && valueFromInitial === void 0 ? void 0 : this.baseTarget[key];
17610
- }
17611
- on(eventName, callback) {
17612
- if (!this.events[eventName]) {
17613
- this.events[eventName] = new SubscriptionManager();
17614
- }
17615
- return this.events[eventName].add(callback);
17616
- }
17617
- notify(eventName, ...args) {
17618
- if (this.events[eventName]) {
17619
- this.events[eventName].notify(...args);
17620
- }
17621
- }
17622
- }
17623
- class DOMVisualElement extends VisualElement {
17624
- constructor() {
17625
- super(...arguments);
17626
- this.KeyframeResolver = DOMKeyframesResolver;
17627
- }
17628
- sortInstanceNodePosition(a2, b2) {
17629
- return a2.compareDocumentPosition(b2) & 2 ? 1 : -1;
17630
- }
17631
- getBaseTargetFromProps(props, key) {
17632
- return props.style ? props.style[key] : void 0;
17633
- }
17634
- removeValueFromRenderState(key, { vars, style }) {
17635
- delete vars[key];
17636
- delete style[key];
17637
- }
17638
- handleChildMotionValue() {
17639
- if (this.childSubscription) {
17640
- this.childSubscription();
17641
- delete this.childSubscription;
17642
- }
17643
- const { children } = this.props;
17644
- if (isMotionValue(children)) {
17645
- this.childSubscription = children.on("change", (latest) => {
17646
- if (this.current) {
17647
- this.current.textContent = `${latest}`;
17648
- }
17649
- });
17650
- }
17651
- }
17652
- }
17653
- function getComputedStyle(element) {
17654
- return window.getComputedStyle(element);
17655
- }
17656
- class HTMLVisualElement extends DOMVisualElement {
17657
- constructor() {
17658
- super(...arguments);
17659
- this.type = "html";
17660
- this.renderInstance = renderHTML;
17661
- }
17662
- readValueFromInstance(instance, key) {
17663
- if (transformProps.has(key)) {
17664
- const defaultType = getDefaultValueType(key);
17665
- return defaultType ? defaultType.default || 0 : 0;
17666
- } else {
17667
- const computedStyle = getComputedStyle(instance);
17668
- const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) : computedStyle[key]) || 0;
17669
- return typeof value === "string" ? value.trim() : value;
17670
- }
17671
- }
17672
- measureInstanceViewportBox(instance, { transformPagePoint }) {
17673
- return measureViewportBox(instance, transformPagePoint);
17674
- }
17675
- build(renderState, latestValues, props) {
17676
- buildHTMLStyles(renderState, latestValues, props.transformTemplate);
17677
- }
17678
- scrapeMotionValuesFromProps(props, prevProps, visualElement) {
17679
- return scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
17680
- }
17681
- }
17682
- class SVGVisualElement extends DOMVisualElement {
17683
- constructor() {
17684
- super(...arguments);
17685
- this.type = "svg";
17686
- this.isSVGTag = false;
17687
- this.measureInstanceViewportBox = createBox;
18601
+ return target;
18602
+ return this.initialValues[key] !== void 0 && valueFromInitial === void 0 ? void 0 : this.baseTarget[key];
17688
18603
  }
17689
- getBaseTargetFromProps(props, key) {
17690
- return props[key];
18604
+ on(eventName, callback) {
18605
+ if (!this.events[eventName]) {
18606
+ this.events[eventName] = new SubscriptionManager();
18607
+ }
18608
+ return this.events[eventName].add(callback);
17691
18609
  }
17692
- readValueFromInstance(instance, key) {
17693
- if (transformProps.has(key)) {
17694
- const defaultType = getDefaultValueType(key);
17695
- return defaultType ? defaultType.default || 0 : 0;
18610
+ notify(eventName, ...args) {
18611
+ if (this.events[eventName]) {
18612
+ this.events[eventName].notify(...args);
17696
18613
  }
17697
- key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
17698
- return instance.getAttribute(key);
17699
18614
  }
17700
- scrapeMotionValuesFromProps(props, prevProps, visualElement) {
17701
- return scrapeMotionValuesFromProps(props, prevProps, visualElement);
18615
+ }
18616
+ class DOMVisualElement extends VisualElement {
18617
+ constructor() {
18618
+ super(...arguments);
18619
+ this.KeyframeResolver = DOMKeyframesResolver;
17702
18620
  }
17703
- build(renderState, latestValues, props) {
17704
- buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate);
18621
+ sortInstanceNodePosition(a2, b2) {
18622
+ return a2.compareDocumentPosition(b2) & 2 ? 1 : -1;
17705
18623
  }
17706
- renderInstance(instance, renderState, styleProp, projection) {
17707
- renderSVG(instance, renderState, styleProp, projection);
18624
+ getBaseTargetFromProps(props, key) {
18625
+ return props.style ? props.style[key] : void 0;
17708
18626
  }
17709
- mount(instance) {
17710
- this.isSVGTag = isSVGTag(instance.tagName);
17711
- super.mount(instance);
18627
+ removeValueFromRenderState(key, { vars, style }) {
18628
+ delete vars[key];
18629
+ delete style[key];
17712
18630
  }
17713
- }
17714
- const createDomVisualElement = (Component2, options) => {
17715
- return isSVGComponent(Component2) ? new SVGVisualElement(options) : new HTMLVisualElement(options, {
17716
- allowProjection: Component2 !== Fragment
17717
- });
17718
- };
17719
- const createMotionComponent = /* @__PURE__ */ createMotionComponentFactory({
17720
- ...animations,
17721
- ...gestureAnimations,
17722
- ...drag,
17723
- ...layout
17724
- }, createDomVisualElement);
17725
- const motion = /* @__PURE__ */ createDOMMotionComponentProxy(createMotionComponent);
17726
- const INITIAL_TIMELINE_DATA = {
17727
- tracks: [
17728
- {
17729
- type: "element",
17730
- id: "t-sample",
17731
- name: "sample",
17732
- elements: [
17733
- {
17734
- id: "e-sample",
17735
- trackId: "t-sample",
17736
- name: "sample",
17737
- type: "text",
17738
- s: 0,
17739
- e: 5,
17740
- props: {
17741
- text: "Twick Video Editor",
17742
- fill: "#FFFFFF"
17743
- }
17744
- }
17745
- ]
18631
+ handleChildMotionValue() {
18632
+ if (this.childSubscription) {
18633
+ this.childSubscription();
18634
+ delete this.childSubscription;
17746
18635
  }
17747
- ],
17748
- version: 1
17749
- };
17750
- const MIN_DURATION = 0.1;
17751
- const DRAG_TYPE = {
17752
- /** Drag operation is starting */
17753
- START: "start",
17754
- /** Drag operation is in progress */
17755
- MOVE: "move",
17756
- /** Drag operation has ended */
17757
- END: "end"
17758
- };
17759
- const DEFAULT_TIMELINE_ZOOM = 1.5;
17760
- const DEFAULT_TIMELINE_ZOOM_CONFIG = {
17761
- /** Minimum zoom level (10%) */
17762
- min: 0.1,
17763
- /** Maximum zoom level (300%) */
17764
- max: 3,
17765
- /** Zoom step increment/decrement (10%) */
17766
- step: 0.1,
17767
- /** Default zoom level (150%) */
17768
- default: 1.5
17769
- };
17770
- const DEFAULT_TIMELINE_TICK_CONFIGS = [
17771
- {
17772
- durationThreshold: 10,
17773
- // < 10 seconds
17774
- majorInterval: 1,
17775
- // 1s major ticks
17776
- minorTicks: 10
17777
- // 0.1s minor ticks (10 minors between majors)
17778
- },
17779
- {
17780
- durationThreshold: 30,
17781
- // < 30 seconds
17782
- majorInterval: 5,
17783
- // 5s major ticks
17784
- minorTicks: 5
17785
- // 1s minor ticks (5 minors between majors)
17786
- },
17787
- {
17788
- durationThreshold: 120,
17789
- // < 2 minutes
17790
- majorInterval: 10,
17791
- // 10s major ticks
17792
- minorTicks: 5
17793
- // 2s minor ticks (5 minors between majors)
17794
- },
17795
- {
17796
- durationThreshold: 300,
17797
- // < 5 minutes
17798
- majorInterval: 30,
17799
- // 30s major ticks
17800
- minorTicks: 6
17801
- // 5s minor ticks (6 minors between majors)
17802
- },
17803
- {
17804
- durationThreshold: 900,
17805
- // < 15 minutes
17806
- majorInterval: 60,
17807
- // 1m major ticks
17808
- minorTicks: 6
17809
- // 10s minor ticks (6 minors between majors)
17810
- },
17811
- {
17812
- durationThreshold: 1800,
17813
- // < 30 minutes
17814
- majorInterval: 120,
17815
- // 2m major ticks
17816
- minorTicks: 4
17817
- // 30s minor ticks (4 minors between majors)
17818
- },
17819
- {
17820
- durationThreshold: 3600,
17821
- // < 1 hour
17822
- majorInterval: 300,
17823
- // 5m major ticks
17824
- minorTicks: 5
17825
- // 1m minor ticks (5 minors between majors)
17826
- },
17827
- {
17828
- durationThreshold: 7200,
17829
- // < 2 hours
17830
- majorInterval: 600,
17831
- // 10m major ticks
17832
- minorTicks: 10
17833
- // 1m minor ticks (10 minors between majors)
17834
- },
17835
- {
17836
- durationThreshold: Infinity,
17837
- // >= 2 hours
17838
- majorInterval: 1800,
17839
- // 30m major ticks
17840
- minorTicks: 6
17841
- // 5m minor ticks (6 minors between majors)
17842
- }
17843
- ];
17844
- const DEFAULT_ELEMENT_COLORS = {
17845
- /** Fragment element color - deep charcoal matching UI background */
17846
- fragment: "#1A1A1A",
17847
- /** Video element color - vibrant royal purple */
17848
- video: "#8B5FBF",
17849
- /** Caption element color - soft wisteria purple */
17850
- caption: "#9B8ACE",
17851
- /** Image element color - warm copper accent */
17852
- image: "#D4956C",
17853
- /** Audio element color - deep teal */
17854
- audio: "#3D8B8B",
17855
- /** Text element color - medium lavender */
17856
- text: "#8D74C4",
17857
- /** Generic element color - muted amethyst */
17858
- element: "#7B68B8",
17859
- /** Rectangle element color - deep indigo */
17860
- rect: "#5B4B99",
17861
- /** Frame effect color - rich magenta */
17862
- frameEffect: "#B55B9C",
17863
- /** Filters color - periwinkle blue */
17864
- filters: "#7A89D4",
17865
- /** Transition color - burnished bronze */
17866
- transition: "#BE8157",
17867
- /** Animation color - muted emerald */
17868
- animation: "#4B9B78",
17869
- /** Icon element color - bright orchid */
17870
- icon: "#A76CD4",
17871
- /** Circle element color - deep byzantium */
17872
- circle: "#703D8B"
17873
- };
17874
- const AVAILABLE_TEXT_FONTS = {
17875
- // Google Fonts
17876
- /** Modern sans-serif font */
17877
- RUBIK: "Rubik",
17878
- /** Clean and readable font */
17879
- MULISH: "Mulish",
17880
- /** Bold display font */
17881
- LUCKIEST_GUY: "Luckiest Guy",
17882
- /** Elegant serif font */
17883
- PLAYFAIR_DISPLAY: "Playfair Display",
17884
- /** Classic sans-serif font */
17885
- ROBOTO: "Roboto",
17886
- /** Modern geometric font */
17887
- POPPINS: "Poppins",
17888
- // Display and Decorative Fonts
17889
- /** Comic-style display font */
17890
- BANGERS: "Bangers",
17891
- /** Handwritten-style font */
17892
- BIRTHSTONE: "Birthstone",
17893
- /** Elegant script font */
17894
- CORINTHIA: "Corinthia",
17895
- /** Formal script font */
17896
- IMPERIAL_SCRIPT: "Imperial Script",
17897
- /** Bold outline font */
17898
- KUMAR_ONE_OUTLINE: "Kumar One Outline",
17899
- /** Light outline font */
17900
- LONDRI_OUTLINE: "Londrina Outline",
17901
- /** Casual script font */
17902
- MARCK_SCRIPT: "Marck Script",
17903
- /** Modern sans-serif font */
17904
- MONTSERRAT: "Montserrat",
17905
- /** Stylish display font */
17906
- PATTAYA: "Pattaya",
17907
- // CDN Fonts
17908
- /** Unique display font */
17909
- PERALTA: "Peralta",
17910
- /** Bold impact font */
17911
- IMPACT: "Impact",
17912
- /** Handwritten-style font */
17913
- LUMANOSIMO: "Lumanosimo",
17914
- /** Custom display font */
17915
- KAPAKANA: "Kapakana",
17916
- /** Handwritten font */
17917
- HANDYRUSH: "HandyRush",
17918
- /** Decorative font */
17919
- DASHER: "Dasher",
17920
- /** Signature-style font */
17921
- BRITTANY_SIGNATURE: "Brittany Signature"
18636
+ const { children } = this.props;
18637
+ if (isMotionValue(children)) {
18638
+ this.childSubscription = children.on("change", (latest) => {
18639
+ if (this.current) {
18640
+ this.current.textContent = `${latest}`;
18641
+ }
18642
+ });
18643
+ }
18644
+ }
18645
+ }
18646
+ function getComputedStyle(element) {
18647
+ return window.getComputedStyle(element);
18648
+ }
18649
+ class HTMLVisualElement extends DOMVisualElement {
18650
+ constructor() {
18651
+ super(...arguments);
18652
+ this.type = "html";
18653
+ this.renderInstance = renderHTML;
18654
+ }
18655
+ readValueFromInstance(instance, key) {
18656
+ if (transformProps.has(key)) {
18657
+ const defaultType = getDefaultValueType(key);
18658
+ return defaultType ? defaultType.default || 0 : 0;
18659
+ } else {
18660
+ const computedStyle = getComputedStyle(instance);
18661
+ const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) : computedStyle[key]) || 0;
18662
+ return typeof value === "string" ? value.trim() : value;
18663
+ }
18664
+ }
18665
+ measureInstanceViewportBox(instance, { transformPagePoint }) {
18666
+ return measureViewportBox(instance, transformPagePoint);
18667
+ }
18668
+ build(renderState, latestValues, props) {
18669
+ buildHTMLStyles(renderState, latestValues, props.transformTemplate);
18670
+ }
18671
+ scrapeMotionValuesFromProps(props, prevProps, visualElement) {
18672
+ return scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
18673
+ }
18674
+ }
18675
+ class SVGVisualElement extends DOMVisualElement {
18676
+ constructor() {
18677
+ super(...arguments);
18678
+ this.type = "svg";
18679
+ this.isSVGTag = false;
18680
+ this.measureInstanceViewportBox = createBox;
18681
+ }
18682
+ getBaseTargetFromProps(props, key) {
18683
+ return props[key];
18684
+ }
18685
+ readValueFromInstance(instance, key) {
18686
+ if (transformProps.has(key)) {
18687
+ const defaultType = getDefaultValueType(key);
18688
+ return defaultType ? defaultType.default || 0 : 0;
18689
+ }
18690
+ key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
18691
+ return instance.getAttribute(key);
18692
+ }
18693
+ scrapeMotionValuesFromProps(props, prevProps, visualElement) {
18694
+ return scrapeMotionValuesFromProps(props, prevProps, visualElement);
18695
+ }
18696
+ build(renderState, latestValues, props) {
18697
+ buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate);
18698
+ }
18699
+ renderInstance(instance, renderState, styleProp, projection) {
18700
+ renderSVG(instance, renderState, styleProp, projection);
18701
+ }
18702
+ mount(instance) {
18703
+ this.isSVGTag = isSVGTag(instance.tagName);
18704
+ super.mount(instance);
18705
+ }
18706
+ }
18707
+ const createDomVisualElement = (Component2, options) => {
18708
+ return isSVGComponent(Component2) ? new SVGVisualElement(options) : new HTMLVisualElement(options, {
18709
+ allowProjection: Component2 !== Fragment
18710
+ });
17922
18711
  };
18712
+ const createMotionComponent = /* @__PURE__ */ createMotionComponentFactory({
18713
+ ...animations,
18714
+ ...gestureAnimations,
18715
+ ...drag,
18716
+ ...layout
18717
+ }, createDomVisualElement);
18718
+ const motion = /* @__PURE__ */ createDOMMotionComponentProxy(createMotionComponent);
17923
18719
  let ELEMENT_COLORS = { ...DEFAULT_ELEMENT_COLORS };
17924
18720
  const setElementColors = (colors) => {
17925
18721
  ELEMENT_COLORS = {
@@ -17934,6 +18730,7 @@ const TrackElementView = ({
17934
18730
  nextStart,
17935
18731
  prevEnd,
17936
18732
  selectedItem,
18733
+ selectedIds,
17937
18734
  onSelection,
17938
18735
  onDrag,
17939
18736
  allowOverlap = false,
@@ -17960,8 +18757,8 @@ const TrackElementView = ({
17960
18757
  dragType.current = DRAG_TYPE.MOVE;
17961
18758
  setPosition((prev) => {
17962
18759
  const span = prev.end - prev.start;
17963
- let newStart = Math.max(0, prev.start + dx / parentWidth * duration);
17964
- newStart = Math.min(newStart, prev.end - MIN_DURATION);
18760
+ let newStart = prev.start + dx / parentWidth * duration;
18761
+ newStart = Math.max(0, Math.min(newStart, prev.end - MIN_DURATION));
17965
18762
  if (!allowOverlap) {
17966
18763
  if (prevEnd !== null && newStart < prevEnd) {
17967
18764
  newStart = prevEnd;
@@ -17985,8 +18782,8 @@ const TrackElementView = ({
17985
18782
  }
17986
18783
  dragType.current = DRAG_TYPE.START;
17987
18784
  setPosition((prev) => {
17988
- let newStart = Math.max(0, prev.start + dx / parentWidth * duration);
17989
- newStart = Math.min(newStart, prev.end - MIN_DURATION);
18785
+ let newStart = prev.start + dx / parentWidth * duration;
18786
+ newStart = Math.max(0, Math.min(newStart, prev.end - MIN_DURATION));
17990
18787
  if (prevEnd !== null && !allowOverlap && newStart < prevEnd) {
17991
18788
  newStart = prevEnd;
17992
18789
  }
@@ -18044,8 +18841,9 @@ const TrackElementView = ({
18044
18841
  return ELEMENT_COLORS.element;
18045
18842
  };
18046
18843
  const isSelected = useMemo(() => {
18047
- return (selectedItem == null ? void 0 : selectedItem.getId()) === element.getId();
18048
- }, [selectedItem, element]);
18844
+ return selectedIds.has(element.getId());
18845
+ }, [selectedIds, element]);
18846
+ const hasHandles = (selectedItem == null ? void 0 : selectedItem.getId()) === element.getId();
18049
18847
  const motionProps = {
18050
18848
  ref,
18051
18849
  className: `twick-track-element ${isSelected ? "twick-track-element-selected" : "twick-track-element-default"} ${isDragging2 ? "twick-track-element-dragging" : ""}`,
@@ -18061,9 +18859,9 @@ const TrackElementView = ({
18061
18859
  },
18062
18860
  onMouseUp: sendUpdate,
18063
18861
  onTouchEnd: sendUpdate,
18064
- onClick: () => {
18862
+ onClick: (e3) => {
18065
18863
  if (onSelection) {
18066
- onSelection(element);
18864
+ onSelection(element, e3);
18067
18865
  }
18068
18866
  },
18069
18867
  style: {
@@ -18074,7 +18872,7 @@ const TrackElementView = ({
18074
18872
  }
18075
18873
  };
18076
18874
  return /* @__PURE__ */ jsx(motion.div, { ...motionProps, children: /* @__PURE__ */ jsxs("div", { style: { touchAction: "none", height: "100%" }, ...bind(), children: [
18077
- isSelected ? /* @__PURE__ */ jsx(
18875
+ hasHandles ? /* @__PURE__ */ jsx(
18078
18876
  "div",
18079
18877
  {
18080
18878
  style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
@@ -18083,7 +18881,7 @@ const TrackElementView = ({
18083
18881
  }
18084
18882
  ) : null,
18085
18883
  /* @__PURE__ */ jsx("div", { className: "twick-track-element-content", children: element.getText ? element.getText() : element.getName() || element.getType() }),
18086
- isSelected ? /* @__PURE__ */ jsx(
18884
+ hasHandles ? /* @__PURE__ */ jsx(
18087
18885
  "div",
18088
18886
  {
18089
18887
  style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
@@ -18113,6 +18911,7 @@ const TrackBase = ({
18113
18911
  track,
18114
18912
  trackWidth,
18115
18913
  selectedItem,
18914
+ selectedIds,
18116
18915
  onItemSelection,
18117
18916
  onDrag,
18118
18917
  allowOverlap = false,
@@ -18137,6 +18936,7 @@ const TrackBase = ({
18137
18936
  allowOverlap,
18138
18937
  parentWidth: trackWidth,
18139
18938
  selectedItem,
18939
+ selectedIds,
18140
18940
  onSelection: onItemSelection,
18141
18941
  onDrag,
18142
18942
  elementColors,
@@ -18148,6 +18948,199 @@ const TrackBase = ({
18148
18948
  }
18149
18949
  );
18150
18950
  };
18951
+ const DEFAULT_MARGIN = 80;
18952
+ function usePlayheadScroll(scrollContainerRef, playheadPositionPx, isActive, config) {
18953
+ const margin = (config == null ? void 0 : config.margin) ?? DEFAULT_MARGIN;
18954
+ const labelWidth = config == null ? void 0 : config.labelWidth;
18955
+ const rafRef = useRef(null);
18956
+ useEffect(() => {
18957
+ if (!isActive || !scrollContainerRef.current) return;
18958
+ const container = scrollContainerRef.current;
18959
+ const contentX = labelWidth + playheadPositionPx;
18960
+ const scrollToKeepPlayheadVisible = () => {
18961
+ const { scrollLeft, clientWidth } = container;
18962
+ const minVisible = scrollLeft + margin;
18963
+ const maxVisible = scrollLeft + clientWidth - margin;
18964
+ let newScrollLeft = null;
18965
+ if (contentX < minVisible) {
18966
+ newScrollLeft = Math.max(0, contentX - margin);
18967
+ } else if (contentX > maxVisible) {
18968
+ newScrollLeft = contentX - clientWidth + margin;
18969
+ }
18970
+ if (newScrollLeft !== null) {
18971
+ container.scrollLeft = newScrollLeft;
18972
+ }
18973
+ };
18974
+ const scheduleScroll = () => {
18975
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
18976
+ rafRef.current = requestAnimationFrame(() => {
18977
+ rafRef.current = null;
18978
+ scrollToKeepPlayheadVisible();
18979
+ });
18980
+ };
18981
+ scheduleScroll();
18982
+ return () => {
18983
+ if (rafRef.current !== null) {
18984
+ cancelAnimationFrame(rafRef.current);
18985
+ }
18986
+ };
18987
+ }, [
18988
+ isActive,
18989
+ playheadPositionPx,
18990
+ scrollContainerRef,
18991
+ margin,
18992
+ labelWidth
18993
+ ]);
18994
+ }
18995
+ const MARQUEE_THRESHOLD = 4;
18996
+ function useMarqueeSelection({
18997
+ duration,
18998
+ zoomLevel,
18999
+ labelWidth,
19000
+ trackCount,
19001
+ trackHeight,
19002
+ tracks,
19003
+ containerRef,
19004
+ onMarqueeSelect,
19005
+ onEmptyClick
19006
+ }) {
19007
+ const [marquee, setMarquee] = useState(null);
19008
+ const startPosRef = useRef(null);
19009
+ const hasDraggedRef = useRef(false);
19010
+ const marqueeRef = useRef(marquee);
19011
+ marqueeRef.current = marquee;
19012
+ const pixelsPerSecond = 100 * zoomLevel;
19013
+ const getCoords = useCallback(
19014
+ (e3) => {
19015
+ var _a, _b;
19016
+ const rect = (_a = containerRef.current) == null ? void 0 : _a.getBoundingClientRect();
19017
+ if (!rect) return { x: 0, y: 0 };
19018
+ return {
19019
+ x: e3.clientX - rect.left + (((_b = containerRef.current) == null ? void 0 : _b.scrollLeft) ?? 0),
19020
+ y: e3.clientY - rect.top
19021
+ };
19022
+ },
19023
+ [containerRef]
19024
+ );
19025
+ const handleGlobalMouseMove = useCallback(
19026
+ (e3) => {
19027
+ if (!startPosRef.current) return;
19028
+ const { x: x2, y: y2 } = getCoords(e3);
19029
+ const dx = Math.abs(x2 - startPosRef.current.x);
19030
+ const dy = Math.abs(y2 - startPosRef.current.y);
19031
+ if (dx > MARQUEE_THRESHOLD || dy > MARQUEE_THRESHOLD) {
19032
+ hasDraggedRef.current = true;
19033
+ }
19034
+ setMarquee((prev) => prev ? { ...prev, endX: x2, endY: y2 } : null);
19035
+ },
19036
+ [getCoords]
19037
+ );
19038
+ const handleGlobalMouseUp = useCallback(() => {
19039
+ if (!startPosRef.current) return;
19040
+ const currentMarquee = marqueeRef.current;
19041
+ if (!hasDraggedRef.current || !currentMarquee) {
19042
+ setMarquee(null);
19043
+ startPosRef.current = null;
19044
+ onEmptyClick();
19045
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
19046
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
19047
+ return;
19048
+ }
19049
+ const left = Math.min(currentMarquee.startX, currentMarquee.endX);
19050
+ const right = Math.max(currentMarquee.startX, currentMarquee.endX);
19051
+ const top = Math.min(currentMarquee.startY, currentMarquee.endY);
19052
+ const bottom = Math.max(currentMarquee.startY, currentMarquee.endY);
19053
+ const startTime = Math.max(0, (left - labelWidth) / pixelsPerSecond);
19054
+ const endTime = Math.min(duration, (right - labelWidth) / pixelsPerSecond);
19055
+ const rowHeight = trackHeight + 2;
19056
+ const startTrackIdx = Math.max(0, Math.floor(top / rowHeight));
19057
+ const endTrackIdx = Math.min(
19058
+ trackCount - 1,
19059
+ Math.floor(bottom / rowHeight)
19060
+ );
19061
+ const selectedIds = /* @__PURE__ */ new Set();
19062
+ for (let tIdx = startTrackIdx; tIdx <= endTrackIdx; tIdx++) {
19063
+ const track = tracks[tIdx];
19064
+ if (!track) continue;
19065
+ for (const el of track.getElements()) {
19066
+ const elStart = el.getStart();
19067
+ const elEnd = el.getEnd();
19068
+ if (elStart < endTime && elEnd > startTime) {
19069
+ selectedIds.add(el.getId());
19070
+ }
19071
+ }
19072
+ }
19073
+ onMarqueeSelect(selectedIds);
19074
+ setMarquee(null);
19075
+ startPosRef.current = null;
19076
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
19077
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
19078
+ }, [
19079
+ duration,
19080
+ pixelsPerSecond,
19081
+ labelWidth,
19082
+ trackCount,
19083
+ trackHeight,
19084
+ tracks,
19085
+ onMarqueeSelect,
19086
+ onEmptyClick,
19087
+ handleGlobalMouseMove
19088
+ ]);
19089
+ useEffect(() => {
19090
+ if (!marquee) return;
19091
+ window.addEventListener("mousemove", handleGlobalMouseMove);
19092
+ window.addEventListener("mouseup", handleGlobalMouseUp);
19093
+ return () => {
19094
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
19095
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
19096
+ };
19097
+ }, [marquee, handleGlobalMouseMove, handleGlobalMouseUp]);
19098
+ const handleMouseDown = useCallback(
19099
+ (e3) => {
19100
+ if (e3.target.closest(".twick-track-element") || e3.target.closest(".twick-track-header")) {
19101
+ return;
19102
+ }
19103
+ const { x: x2, y: y2 } = getCoords(e3.nativeEvent);
19104
+ startPosRef.current = { x: x2, y: y2 };
19105
+ hasDraggedRef.current = false;
19106
+ setMarquee({ startX: x2, startY: y2, endX: x2, endY: y2 });
19107
+ },
19108
+ [getCoords]
19109
+ );
19110
+ return { marquee, handleMouseDown };
19111
+ }
19112
+ function MarqueeOverlay({ marquee }) {
19113
+ return /* @__PURE__ */ jsx(
19114
+ "div",
19115
+ {
19116
+ className: "twick-marquee-overlay",
19117
+ style: {
19118
+ position: "absolute",
19119
+ inset: 0,
19120
+ zIndex: 25,
19121
+ pointerEvents: "none"
19122
+ },
19123
+ children: marquee && /* @__PURE__ */ jsx(
19124
+ "div",
19125
+ {
19126
+ className: "twick-marquee-rect",
19127
+ style: {
19128
+ position: "absolute",
19129
+ left: Math.min(marquee.startX, marquee.endX),
19130
+ top: Math.min(marquee.startY, marquee.endY),
19131
+ width: Math.abs(marquee.endX - marquee.startX),
19132
+ height: Math.abs(marquee.endY - marquee.startY),
19133
+ border: "1px solid rgba(255, 255, 255, 0.7)",
19134
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
19135
+ pointerEvents: "none"
19136
+ }
19137
+ }
19138
+ )
19139
+ }
19140
+ );
19141
+ }
19142
+ const LABEL_WIDTH = 40;
19143
+ const TRACK_HEIGHT = 44;
18151
19144
  function TimelineView({
18152
19145
  zoomLevel,
18153
19146
  selectedItem,
@@ -18156,20 +19149,28 @@ function TimelineView({
18156
19149
  seekTrack,
18157
19150
  onAddTrack,
18158
19151
  onReorder,
18159
- onSelectionChange,
19152
+ onItemSelect,
19153
+ onEmptyClick,
19154
+ onMarqueeSelect,
18160
19155
  onElementDrag,
18161
- elementColors
19156
+ elementColors,
19157
+ selectedIds,
19158
+ playheadPositionPx = 0,
19159
+ isPlayheadActive = false,
19160
+ onDropOnTimeline,
19161
+ videoResolution,
19162
+ enableDropOnTimeline = true
18162
19163
  }) {
18163
19164
  const containerRef = useRef(null);
18164
19165
  const seekContainerRef = useRef(null);
18165
19166
  const timelineContentRef = useRef(null);
18166
19167
  const [, setScrollLeft] = useState(0);
18167
19168
  const [draggedTimeline, setDraggedTimeline] = useState(null);
18168
- const { selectedTrack, selectedTrackElement } = useMemo(() => {
19169
+ const { selectedTrackElement } = useMemo(() => {
18169
19170
  if (selectedItem && "elements" in selectedItem) {
18170
- return { selectedTrack: selectedItem, selectedTrackElement: null };
19171
+ return { selectedTrackElement: null };
18171
19172
  }
18172
- return { selectedTrack: null, selectedTrackElement: selectedItem };
19173
+ return { selectedTrackElement: selectedItem };
18173
19174
  }, [selectedItem]);
18174
19175
  const timelineWidth = Math.max(100, duration * zoomLevel * 100);
18175
19176
  const timelineWidthPx = `${timelineWidth}px`;
@@ -18199,7 +19200,33 @@ function TimelineView({
18199
19200
  window.removeEventListener("resize", updateWidth);
18200
19201
  };
18201
19202
  }, [duration, zoomLevel]);
18202
- const labelWidth = 140;
19203
+ usePlayheadScroll(containerRef, playheadPositionPx, isPlayheadActive, {
19204
+ labelWidth: LABEL_WIDTH
19205
+ });
19206
+ const { marquee, handleMouseDown: handleMarqueeMouseDown } = useMarqueeSelection({
19207
+ duration,
19208
+ zoomLevel,
19209
+ labelWidth: LABEL_WIDTH,
19210
+ trackCount: (tracks == null ? void 0 : tracks.length) ?? 0,
19211
+ trackHeight: TRACK_HEIGHT,
19212
+ tracks: tracks ?? [],
19213
+ containerRef: timelineContentRef,
19214
+ onMarqueeSelect,
19215
+ onEmptyClick
19216
+ });
19217
+ const { preview, handleDragOver, handleDragLeave, handleDrop } = useTimelineDrop({
19218
+ containerRef: timelineContentRef,
19219
+ scrollContainerRef: containerRef,
19220
+ tracks: tracks ?? [],
19221
+ duration,
19222
+ zoomLevel,
19223
+ labelWidth: LABEL_WIDTH,
19224
+ trackHeight: TRACK_HEIGHT,
19225
+ trackContentWidth: timelineWidth - LABEL_WIDTH,
19226
+ onDrop: onDropOnTimeline ?? (async () => {
19227
+ }),
19228
+ enabled: enableDropOnTimeline && !!onDropOnTimeline && !!videoResolution
19229
+ });
18203
19230
  const handleTrackDragStart = (e3, track) => {
18204
19231
  setDraggedTimeline(track);
18205
19232
  e3.dataTransfer.setData("application/json", JSON.stringify(track));
@@ -18231,10 +19258,8 @@ function TimelineView({
18231
19258
  }
18232
19259
  setDraggedTimeline(null);
18233
19260
  };
18234
- const handleItemSelection = (element) => {
18235
- if (onSelectionChange) {
18236
- onSelectionChange(element);
18237
- }
19261
+ const handleItemSelection = (item, event) => {
19262
+ onItemSelect(item, event);
18238
19263
  };
18239
19264
  return /* @__PURE__ */ jsxs(
18240
19265
  "div",
@@ -18247,44 +19272,106 @@ function TimelineView({
18247
19272
  /* @__PURE__ */ jsx("div", { className: "twick-seek-track-empty-space", onClick: onAddTrack, children: /* @__PURE__ */ jsx(Plus, { color: "white", size: 20 }) }),
18248
19273
  /* @__PURE__ */ jsx("div", { style: { flexGrow: 1 }, children: seekTrack })
18249
19274
  ] }) : null }),
18250
- /* @__PURE__ */ jsx("div", { ref: timelineContentRef, style: { width: timelineWidthPx }, children: (tracks || []).map((track) => /* @__PURE__ */ jsxs("div", { className: "twick-timeline-container", children: [
18251
- /* @__PURE__ */ jsx("div", { className: "twick-timeline-header-container", children: /* @__PURE__ */ jsx(
18252
- TrackHeader,
18253
- {
18254
- track,
18255
- selectedItem: selectedTrack,
18256
- onSelect: handleItemSelection,
18257
- onDragStart: handleTrackDragStart,
18258
- onDragOver: handleTrackDragOver,
18259
- onDrop: handleTrackDrop
18260
- }
18261
- ) }),
18262
- /* @__PURE__ */ jsx(
18263
- TrackBase,
18264
- {
18265
- track,
18266
- duration,
18267
- selectedItem: selectedTrackElement,
18268
- zoom: zoomLevel,
18269
- allowOverlap: false,
18270
- trackWidth: timelineWidth - labelWidth,
18271
- onItemSelection: handleItemSelection,
18272
- onDrag: onElementDrag,
18273
- elementColors
18274
- }
18275
- )
18276
- ] }, track.getId())) })
19275
+ /* @__PURE__ */ jsxs(
19276
+ "div",
19277
+ {
19278
+ ref: timelineContentRef,
19279
+ style: { width: timelineWidthPx, position: "relative" },
19280
+ onMouseDown: handleMarqueeMouseDown,
19281
+ onDragOver: handleDragOver,
19282
+ onDragLeave: handleDragLeave,
19283
+ onDrop: handleDrop,
19284
+ children: [
19285
+ /* @__PURE__ */ jsx(MarqueeOverlay, { marquee }),
19286
+ preview && /* @__PURE__ */ jsx(
19287
+ "div",
19288
+ {
19289
+ className: "twick-drop-preview",
19290
+ style: {
19291
+ position: "absolute",
19292
+ left: LABEL_WIDTH + preview.timeSec / duration * (timelineWidth - LABEL_WIDTH),
19293
+ top: preview.trackIndex * TRACK_HEIGHT + 2,
19294
+ width: preview.widthPct / 100 * (timelineWidth - LABEL_WIDTH),
19295
+ height: TRACK_HEIGHT - 4
19296
+ }
19297
+ }
19298
+ ),
19299
+ /* @__PURE__ */ jsx("div", { style: { position: "relative", zIndex: 10 }, children: (tracks || []).map((track) => /* @__PURE__ */ jsxs("div", { className: "twick-timeline-container", children: [
19300
+ /* @__PURE__ */ jsx("div", { className: "twick-timeline-header-container", children: /* @__PURE__ */ jsx(
19301
+ TrackHeader,
19302
+ {
19303
+ track,
19304
+ selectedIds,
19305
+ onSelect: handleItemSelection,
19306
+ onDragStart: handleTrackDragStart,
19307
+ onDragOver: handleTrackDragOver,
19308
+ onDrop: handleTrackDrop
19309
+ }
19310
+ ) }),
19311
+ /* @__PURE__ */ jsx(
19312
+ TrackBase,
19313
+ {
19314
+ track,
19315
+ duration,
19316
+ selectedItem: selectedTrackElement,
19317
+ selectedIds,
19318
+ zoom: zoomLevel,
19319
+ allowOverlap: false,
19320
+ trackWidth: timelineWidth - LABEL_WIDTH,
19321
+ onItemSelection: handleItemSelection,
19322
+ onDrag: onElementDrag,
19323
+ elementColors
19324
+ }
19325
+ )
19326
+ ] }, track.getId())) })
19327
+ ]
19328
+ }
19329
+ )
18277
19330
  ]
18278
19331
  }
18279
19332
  );
18280
19333
  }
18281
19334
  const useTimelineManager = () => {
18282
- const { selectedItem, changeLog, setSelectedItem, totalDuration, editor } = useTimelineContext();
19335
+ const { selectedItem, changeLog, setSelectedItem, totalDuration, editor, selectedIds } = useTimelineContext();
18283
19336
  const onElementDrag = ({
18284
19337
  element,
18285
19338
  dragType,
18286
19339
  updates
18287
19340
  }) => {
19341
+ var _a;
19342
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
19343
+ const duration = totalDuration;
19344
+ if (dragType === DRAG_TYPE.MOVE && selectedIds.has(element.getId()) && selectedIds.size > 1) {
19345
+ const resolved = resolveIds(selectedIds, tracks);
19346
+ const elements = resolved.filter((item) => item instanceof TrackElement);
19347
+ if (elements.length > 1) {
19348
+ const minStart = Math.min(...elements.map((el) => el.getStart()));
19349
+ const maxEnd = Math.max(...elements.map((el) => el.getEnd()));
19350
+ const delta = updates.start - element.getStart();
19351
+ const deltaMin = -minStart;
19352
+ const deltaMax = duration - maxEnd;
19353
+ const clampedDelta = Math.max(deltaMin, Math.min(deltaMax, delta));
19354
+ for (const el of elements) {
19355
+ const newStart = el.getStart() + clampedDelta;
19356
+ const newEnd = el.getEnd() + clampedDelta;
19357
+ if (el instanceof VideoElement$1 || el instanceof AudioElement) {
19358
+ const elementProps = el.getProps();
19359
+ const startDelta = newStart - el.getStart() * ((elementProps == null ? void 0 : elementProps.playbackRate) || 1);
19360
+ if (el instanceof AudioElement) {
19361
+ el.setStartAt(el.getStartAt() + startDelta);
19362
+ } else {
19363
+ el.setStartAt(el.getStartAt() + startDelta);
19364
+ }
19365
+ }
19366
+ el.setStart(newStart);
19367
+ el.setEnd(newEnd);
19368
+ editor.updateElement(el);
19369
+ }
19370
+ setSelectedItem(element);
19371
+ editor.refresh();
19372
+ return;
19373
+ }
19374
+ }
18288
19375
  if (dragType === DRAG_TYPE.START) {
18289
19376
  if (element instanceof VideoElement$1 || element instanceof AudioElement) {
18290
19377
  const elementProps = element.getProps();
@@ -18331,13 +19418,135 @@ const useTimelineManager = () => {
18331
19418
  totalDuration
18332
19419
  };
18333
19420
  };
19421
+ function useTimelineSelection() {
19422
+ const { editor, selectedIds, setSelection, setSelectedItem } = useTimelineContext();
19423
+ const handleItemSelect = useCallback(
19424
+ (item, event) => {
19425
+ var _a;
19426
+ const id2 = item.getId();
19427
+ const isMulti = event.metaKey || event.ctrlKey;
19428
+ const isRange = event.shiftKey;
19429
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
19430
+ if (isMulti) {
19431
+ setSelection((prev) => {
19432
+ const next = new Set(prev);
19433
+ if (next.has(id2)) {
19434
+ next.delete(id2);
19435
+ } else {
19436
+ next.add(id2);
19437
+ }
19438
+ return next;
19439
+ });
19440
+ return;
19441
+ }
19442
+ if (isRange) {
19443
+ const primaryId = selectedIds.size > 0 ? [...selectedIds][0] : null;
19444
+ if (!primaryId) {
19445
+ setSelectedItem(item);
19446
+ return;
19447
+ }
19448
+ const primary = resolveId(primaryId, tracks);
19449
+ if (!primary) {
19450
+ setSelectedItem(item);
19451
+ return;
19452
+ }
19453
+ if (item instanceof Track && primary instanceof Track) {
19454
+ const trackIds = tracks.map((t2) => t2.getId());
19455
+ const fromIdx = trackIds.indexOf(primary.getId());
19456
+ const toIdx = trackIds.indexOf(item.getId());
19457
+ if (fromIdx !== -1 && toIdx !== -1) {
19458
+ const [start, end] = fromIdx <= toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
19459
+ const rangeIds = new Set(
19460
+ trackIds.slice(start, end + 1)
19461
+ );
19462
+ setSelection((prev) => /* @__PURE__ */ new Set([...prev, ...rangeIds]));
19463
+ return;
19464
+ }
19465
+ }
19466
+ if (item instanceof TrackElement && primary instanceof TrackElement) {
19467
+ const track = editor.getTrackById(item.getTrackId());
19468
+ const primaryTrack = editor.getTrackById(primary.getTrackId());
19469
+ if (track && primaryTrack && track.getId() === primaryTrack.getId()) {
19470
+ const rangeIds = getElementIdsInRange(
19471
+ track,
19472
+ primary.getId(),
19473
+ item.getId()
19474
+ );
19475
+ setSelection((prev) => /* @__PURE__ */ new Set([...prev, ...rangeIds]));
19476
+ return;
19477
+ }
19478
+ }
19479
+ }
19480
+ setSelectedItem(item);
19481
+ },
19482
+ [editor, selectedIds, setSelection, setSelectedItem]
19483
+ );
19484
+ const handleEmptyClick = useCallback(() => {
19485
+ setSelectedItem(null);
19486
+ }, [setSelectedItem]);
19487
+ const handleMarqueeSelect = useCallback(
19488
+ (ids) => {
19489
+ setSelection(ids);
19490
+ },
19491
+ [setSelection]
19492
+ );
19493
+ return { handleItemSelect, handleEmptyClick, handleMarqueeSelect };
19494
+ }
18334
19495
  const TimelineManager = ({
18335
19496
  trackZoom,
18336
19497
  timelineTickConfigs,
18337
19498
  elementColors
18338
19499
  }) => {
18339
19500
  var _a;
18340
- const { timelineData, totalDuration, selectedItem, onAddTrack, onReorder, onElementDrag, onSeek, onSelectionChange } = useTimelineManager();
19501
+ const { playerState } = useLivePlayerContext();
19502
+ const { followPlayheadEnabled, editor, videoResolution, setSelectedItem } = useTimelineContext();
19503
+ const {
19504
+ timelineData,
19505
+ totalDuration,
19506
+ selectedItem,
19507
+ onAddTrack,
19508
+ onReorder,
19509
+ onElementDrag,
19510
+ onSeek
19511
+ } = useTimelineManager();
19512
+ const { selectedIds } = useTimelineContext();
19513
+ const { handleItemSelect, handleEmptyClick, handleMarqueeSelect } = useTimelineSelection();
19514
+ const [playheadState, setPlayheadState] = useState({
19515
+ positionPx: 0,
19516
+ isDragging: false
19517
+ });
19518
+ const handlePlayheadUpdate = useCallback((state) => {
19519
+ setPlayheadState(state);
19520
+ }, []);
19521
+ const isPlayheadActive = followPlayheadEnabled && playerState === PLAYER_STATE.PLAYING || playheadState.isDragging;
19522
+ const handleDropOnTimeline = useCallback(
19523
+ async (params) => {
19524
+ const { track, timeSec, type, url } = params;
19525
+ const element = createElementFromDrop(type, url, videoResolution);
19526
+ element.setStart(timeSec);
19527
+ const targetTrack = track ?? editor.addTrack(`Track_${Date.now()}`);
19528
+ const tryAdd = async (t2) => {
19529
+ var _a2;
19530
+ try {
19531
+ const result = await editor.addElementToTrack(t2, element);
19532
+ if (result) {
19533
+ setSelectedItem(element);
19534
+ return true;
19535
+ }
19536
+ } catch (err) {
19537
+ if (err instanceof ValidationError && ((_a2 = err.errors) == null ? void 0 : _a2.includes(VALIDATION_ERROR_CODE.COLLISION_ERROR))) {
19538
+ const newTrack = editor.addTrack(`Track_${Date.now()}`);
19539
+ return tryAdd(newTrack);
19540
+ }
19541
+ throw err;
19542
+ }
19543
+ return false;
19544
+ };
19545
+ await tryAdd(targetTrack);
19546
+ editor.refresh();
19547
+ },
19548
+ [editor, videoResolution, setSelectedItem]
19549
+ );
18341
19550
  return /* @__PURE__ */ jsx(
18342
19551
  TimelineView,
18343
19552
  {
@@ -18345,14 +19554,22 @@ const TimelineManager = ({
18345
19554
  zoomLevel: trackZoom,
18346
19555
  duration: totalDuration,
18347
19556
  selectedItem,
19557
+ selectedIds,
18348
19558
  onDeletion: () => {
18349
19559
  },
18350
19560
  onAddTrack,
18351
19561
  onReorder,
18352
19562
  onElementDrag,
18353
19563
  onSeek,
18354
- onSelectionChange,
19564
+ onItemSelect: handleItemSelect,
19565
+ onEmptyClick: handleEmptyClick,
19566
+ onMarqueeSelect: handleMarqueeSelect,
18355
19567
  elementColors,
19568
+ playheadPositionPx: playheadState.positionPx,
19569
+ isPlayheadActive,
19570
+ onDropOnTimeline: handleDropOnTimeline,
19571
+ videoResolution,
19572
+ enableDropOnTimeline: true,
18356
19573
  seekTrack: /* @__PURE__ */ jsx(
18357
19574
  SeekControl,
18358
19575
  {
@@ -18360,7 +19577,8 @@ const TimelineManager = ({
18360
19577
  zoom: trackZoom,
18361
19578
  onSeek,
18362
19579
  timelineCount: ((_a = timelineData == null ? void 0 : timelineData.tracks) == null ? void 0 : _a.length) ?? 0,
18363
- timelineTickConfigs
19580
+ timelineTickConfigs,
19581
+ onPlayheadUpdate: handlePlayheadUpdate
18364
19582
  }
18365
19583
  )
18366
19584
  }
@@ -18390,6 +19608,7 @@ const UndoRedoControls = ({ canUndo, canRedo, onUndo, onRedo }) => {
18390
19608
  };
18391
19609
  const PlayerControls = ({
18392
19610
  selectedItem,
19611
+ selectedIds = /* @__PURE__ */ new Set(),
18393
19612
  duration,
18394
19613
  currentTime,
18395
19614
  playerState,
@@ -18403,21 +19622,31 @@ const PlayerControls = ({
18403
19622
  zoomLevel = 1,
18404
19623
  setZoomLevel,
18405
19624
  className = "",
18406
- zoomConfig = DEFAULT_TIMELINE_ZOOM_CONFIG
19625
+ zoomConfig = DEFAULT_TIMELINE_ZOOM_CONFIG,
19626
+ fps = DEFAULT_FPS,
19627
+ onSeek,
19628
+ followPlayheadEnabled = true,
19629
+ onFollowPlayheadToggle
18407
19630
  }) => {
18408
19631
  const MAX_ZOOM = zoomConfig.max;
18409
19632
  const MIN_ZOOM = zoomConfig.min;
18410
19633
  const ZOOM_STEP = zoomConfig.step;
18411
- const formatTime = useCallback((time2) => {
18412
- const minutes = Math.floor(time2 / 60);
18413
- const seconds = Math.floor(time2 % 60);
18414
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
18415
- }, []);
19634
+ const formatTime = useCallback(
19635
+ (time2) => formatTimeWithFrames(time2, fps),
19636
+ [fps]
19637
+ );
19638
+ const handleSeekToStart = useCallback(() => {
19639
+ onSeek == null ? void 0 : onSeek(0);
19640
+ }, [onSeek]);
19641
+ const handleSeekToEnd = useCallback(() => {
19642
+ onSeek == null ? void 0 : onSeek(duration);
19643
+ }, [onSeek, duration]);
18416
19644
  const handleDelete = useCallback(() => {
18417
- if (selectedItem && onDelete) {
18418
- onDelete(selectedItem);
19645
+ if (selectedIds.size > 0 && onDelete) {
19646
+ onDelete();
18419
19647
  }
18420
- }, [selectedItem, onDelete]);
19648
+ }, [selectedIds.size, onDelete]);
19649
+ const hasSelection = selectedIds.size > 0;
18421
19650
  const handleSplit = useCallback(() => {
18422
19651
  if (selectedItem instanceof TrackElement && onSplit) {
18423
19652
  onSplit(selectedItem, currentTime);
@@ -18439,9 +19668,9 @@ const PlayerControls = ({
18439
19668
  "button",
18440
19669
  {
18441
19670
  onClick: handleDelete,
18442
- disabled: !selectedItem,
19671
+ disabled: !hasSelection,
18443
19672
  title: "Delete",
18444
- className: `control-btn delete-btn ${!selectedItem ? "btn-disabled" : ""}`,
19673
+ className: `control-btn delete-btn ${!hasSelection ? "btn-disabled" : ""}`,
18445
19674
  children: /* @__PURE__ */ jsx(Trash2, { className: "icon-md" })
18446
19675
  }
18447
19676
  ),
@@ -18466,6 +19695,25 @@ const PlayerControls = ({
18466
19695
  )
18467
19696
  ] }),
18468
19697
  /* @__PURE__ */ jsxs("div", { className: "playback-controls", children: [
19698
+ onFollowPlayheadToggle && /* @__PURE__ */ jsx(
19699
+ "button",
19700
+ {
19701
+ onClick: onFollowPlayheadToggle,
19702
+ title: followPlayheadEnabled ? "Follow playhead on (click to disable)" : "Follow playhead off (click to enable)",
19703
+ className: `control-btn ${followPlayheadEnabled ? "follow-btn-active" : ""}`,
19704
+ children: /* @__PURE__ */ jsx(Crosshair, { className: "icon-md" })
19705
+ }
19706
+ ),
19707
+ /* @__PURE__ */ jsx(
19708
+ "button",
19709
+ {
19710
+ onClick: handleSeekToStart,
19711
+ disabled: playerState === PLAYER_STATE.REFRESH,
19712
+ title: "Jump to start",
19713
+ className: "control-btn",
19714
+ children: /* @__PURE__ */ jsx(SkipBack, { className: "icon-md" })
19715
+ }
19716
+ ),
18469
19717
  /* @__PURE__ */ jsx(
18470
19718
  "button",
18471
19719
  {
@@ -18476,6 +19724,16 @@ const PlayerControls = ({
18476
19724
  children: playerState === PLAYER_STATE.PLAYING ? /* @__PURE__ */ jsx(Pause, { className: "icon-lg" }) : playerState === PLAYER_STATE.REFRESH ? /* @__PURE__ */ jsx(LoaderCircle, { className: "icon-lg animate-spin" }) : /* @__PURE__ */ jsx(Play, { className: "icon-lg" })
18477
19725
  }
18478
19726
  ),
19727
+ /* @__PURE__ */ jsx(
19728
+ "button",
19729
+ {
19730
+ onClick: handleSeekToEnd,
19731
+ disabled: playerState === PLAYER_STATE.REFRESH,
19732
+ title: "Jump to end",
19733
+ className: "control-btn",
19734
+ children: /* @__PURE__ */ jsx(SkipForward, { className: "icon-md" })
19735
+ }
19736
+ ),
18479
19737
  /* @__PURE__ */ jsxs("div", { className: "time-display", children: [
18480
19738
  /* @__PURE__ */ jsx("span", { className: "current-time", children: formatTime(currentTime) }),
18481
19739
  /* @__PURE__ */ jsx("span", { className: "time-separator", children: "/" }),
@@ -18537,12 +19795,17 @@ const usePlayerControl = () => {
18537
19795
  };
18538
19796
  };
18539
19797
  const useTimelineControl = () => {
18540
- const { editor, setSelectedItem } = useTimelineContext();
19798
+ const { editor, setSelectedItem, selectedIds } = useTimelineContext();
18541
19799
  const deleteItem = (item) => {
18542
- if (item instanceof Track) {
18543
- editor.removeTrack(item);
18544
- } else if (item instanceof TrackElement) {
18545
- editor.removeElement(item);
19800
+ var _a;
19801
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
19802
+ const toDelete = item !== void 0 ? [item] : resolveIds(selectedIds, tracks);
19803
+ for (const el of toDelete) {
19804
+ if (el instanceof Track) {
19805
+ editor.removeTrack(el);
19806
+ } else if (el instanceof TrackElement) {
19807
+ editor.removeElement(el);
19808
+ }
18546
19809
  }
18547
19810
  setSelectedItem(null);
18548
19811
  };
@@ -18562,32 +19825,97 @@ const useTimelineControl = () => {
18562
19825
  handleRedo
18563
19826
  };
18564
19827
  };
19828
+ function shouldIgnoreKeydown() {
19829
+ const active = document.activeElement;
19830
+ if (!active) return false;
19831
+ const tag = active.tagName.toLowerCase();
19832
+ if (tag === "input" || tag === "textarea") return true;
19833
+ if (active.isContentEditable) return true;
19834
+ return false;
19835
+ }
19836
+ function useCanvasKeyboard({
19837
+ onDelete,
19838
+ onUndo,
19839
+ onRedo,
19840
+ enabled = true
19841
+ }) {
19842
+ useEffect(() => {
19843
+ if (!enabled) return;
19844
+ const handleKeyDown = (e3) => {
19845
+ if (shouldIgnoreKeydown()) return;
19846
+ const key = e3.key.toLowerCase();
19847
+ const hasPrimaryModifier = e3.metaKey || e3.ctrlKey;
19848
+ if (hasPrimaryModifier) {
19849
+ if (key === "z" && !e3.shiftKey) {
19850
+ e3.preventDefault();
19851
+ onUndo == null ? void 0 : onUndo();
19852
+ return;
19853
+ }
19854
+ if (key === "y" || key === "z" && e3.shiftKey) {
19855
+ e3.preventDefault();
19856
+ onRedo == null ? void 0 : onRedo();
19857
+ return;
19858
+ }
19859
+ }
19860
+ if (!hasPrimaryModifier && (key === "delete" || key === "backspace")) {
19861
+ e3.preventDefault();
19862
+ onDelete == null ? void 0 : onDelete();
19863
+ }
19864
+ };
19865
+ document.addEventListener("keydown", handleKeyDown);
19866
+ return () => document.removeEventListener("keydown", handleKeyDown);
19867
+ }, [enabled, onDelete]);
19868
+ }
18565
19869
  const ControlManager = ({
18566
19870
  trackZoom,
18567
19871
  setTrackZoom,
18568
- zoomConfig
19872
+ zoomConfig,
19873
+ fps
18569
19874
  }) => {
18570
- const { currentTime, playerState } = useLivePlayerContext();
19875
+ const { currentTime, playerState, setSeekTime, setCurrentTime } = useLivePlayerContext();
18571
19876
  const { togglePlayback } = usePlayerControl();
18572
- const { canRedo, canUndo, totalDuration, selectedItem } = useTimelineContext();
19877
+ const {
19878
+ canRedo,
19879
+ canUndo,
19880
+ totalDuration,
19881
+ selectedItem,
19882
+ selectedIds,
19883
+ followPlayheadEnabled,
19884
+ setFollowPlayheadEnabled
19885
+ } = useTimelineContext();
18573
19886
  const { deleteItem, splitElement, handleUndo, handleRedo } = useTimelineControl();
19887
+ useCanvasKeyboard({
19888
+ onDelete: () => deleteItem(),
19889
+ onUndo: () => handleUndo(),
19890
+ onRedo: () => handleRedo()
19891
+ });
19892
+ const handleSeek = (time2) => {
19893
+ const clamped = Math.max(0, Math.min(totalDuration, time2));
19894
+ setCurrentTime(clamped);
19895
+ setSeekTime(clamped);
19896
+ };
18574
19897
  return /* @__PURE__ */ jsx("div", { className: "twick-editor-timeline-controls", children: /* @__PURE__ */ jsx(
18575
19898
  PlayerControls,
18576
19899
  {
18577
19900
  selectedItem,
19901
+ selectedIds,
18578
19902
  duration: totalDuration,
18579
19903
  currentTime,
18580
19904
  playerState,
18581
19905
  togglePlayback,
18582
19906
  canUndo,
18583
19907
  canRedo,
18584
- onDelete: deleteItem,
19908
+ onDelete: () => deleteItem(),
18585
19909
  onSplit: splitElement,
18586
19910
  onUndo: handleUndo,
18587
19911
  onRedo: handleRedo,
18588
19912
  zoomLevel: trackZoom,
18589
19913
  setZoomLevel: setTrackZoom,
18590
- zoomConfig
19914
+ zoomConfig,
19915
+ fps: fps ?? DEFAULT_FPS,
19916
+ onSeek: handleSeek,
19917
+ followPlayheadEnabled,
19918
+ onFollowPlayheadToggle: () => setFollowPlayheadEnabled(!followPlayheadEnabled)
18591
19919
  }
18592
19920
  ) });
18593
19921
  };
@@ -18614,7 +19942,8 @@ const VideoEditor = ({
18614
19942
  {
18615
19943
  videoProps: editorConfig.videoProps,
18616
19944
  playerProps: editorConfig.playerProps,
18617
- canvasMode: editorConfig.canvasMode ?? true
19945
+ canvasMode: editorConfig.canvasMode ?? true,
19946
+ canvasConfig: editorConfig.canvasConfig
18618
19947
  }
18619
19948
  ),
18620
19949
  [editorConfig]
@@ -18632,7 +19961,8 @@ const VideoEditor = ({
18632
19961
  {
18633
19962
  trackZoom,
18634
19963
  setTrackZoom,
18635
- zoomConfig
19964
+ zoomConfig,
19965
+ fps: editorConfig.fps
18636
19966
  }
18637
19967
  ) : null,
18638
19968
  /* @__PURE__ */ jsx(
@@ -19037,6 +20367,7 @@ export {
19037
20367
  BaseMediaManager,
19038
20368
  BrowserMediaManager,
19039
20369
  DEFAULT_ELEMENT_COLORS,
20370
+ DEFAULT_FPS,
19040
20371
  DEFAULT_TIMELINE_TICK_CONFIGS,
19041
20372
  DEFAULT_TIMELINE_ZOOM,
19042
20373
  DEFAULT_TIMELINE_ZOOM_CONFIG,
@@ -19044,7 +20375,9 @@ export {
19044
20375
  INITIAL_TIMELINE_DATA,
19045
20376
  MIN_DURATION,
19046
20377
  PlayerControls,
20378
+ SNAP_THRESHOLD_PX,
19047
20379
  TEXT_EFFECTS,
20380
+ TIMELINE_DROP_MEDIA_TYPE,
19048
20381
  TimelineManager,
19049
20382
  animationGifs,
19050
20383
  VideoEditor as default,