@twick/video-editor 0.15.13 → 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 +2539 -859
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +2543 -863
  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.js CHANGED
@@ -6841,6 +6841,9 @@ class bh extends Va {
6841
6841
  }
6842
6842
  }
6843
6843
  t(bh, "type", "Vibrance"), t(bh, "defaults", { vibrance: 0 }), t(bh, "uniformLocations", ["uVibrance"]), tt.setClass(bh);
6844
+ var __defProp2 = Object.defineProperty;
6845
+ var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6846
+ var __publicField2 = (obj, key, value) => __defNormalProp2(obj, key + "", value);
6844
6847
  const DEFAULT_TEXT_PROPS = {
6845
6848
  /** Font family for text elements */
6846
6849
  family: "Poppins",
@@ -6879,7 +6882,13 @@ const CANVAS_OPERATIONS = {
6879
6882
  /** An item has been updated/modified on the canvas */
6880
6883
  ITEM_UPDATED: "ITEM_UPDATED",
6881
6884
  /** Caption properties have been updated */
6882
- CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED"
6885
+ CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED",
6886
+ /** Watermark has been updated */
6887
+ WATERMARK_UPDATED: "WATERMARK_UPDATED",
6888
+ /** A new element was added via drop on canvas; payload is { element } */
6889
+ ADDED_NEW_ELEMENT: "ADDED_NEW_ELEMENT",
6890
+ /** Z-order changed (bring to front / send to back). Payload is { elementId, direction }. Timeline should reorder tracks. */
6891
+ Z_ORDER_CHANGED: "Z_ORDER_CHANGED"
6883
6892
  };
6884
6893
  const ELEMENT_TYPES = {
6885
6894
  /** Text element type */
@@ -6955,6 +6964,26 @@ const createCanvas = ({
6955
6964
  canvasMetadata
6956
6965
  };
6957
6966
  };
6967
+ function measureTextWidth(text, options) {
6968
+ if (typeof document === "undefined" || !text) return 0;
6969
+ const canvas = document.createElement("canvas");
6970
+ const ctx = canvas.getContext("2d");
6971
+ if (!ctx) return 0;
6972
+ const {
6973
+ fontSize,
6974
+ fontFamily,
6975
+ fontStyle = "normal",
6976
+ fontWeight = "normal"
6977
+ } = options;
6978
+ ctx.font = `${fontStyle} ${String(fontWeight)} ${fontSize}px ${fontFamily}`;
6979
+ const lines = text.split("\n");
6980
+ let maxWidth = 0;
6981
+ for (const line of lines) {
6982
+ const { width } = ctx.measureText(line);
6983
+ if (width > maxWidth) maxWidth = width;
6984
+ }
6985
+ return Math.ceil(maxWidth);
6986
+ }
6958
6987
  const reorderElementsByZIndex = (canvas) => {
6959
6988
  if (!canvas) return;
6960
6989
  const backgroundColor = canvas.backgroundColor;
@@ -6965,6 +6994,49 @@ const reorderElementsByZIndex = (canvas) => {
6965
6994
  objects.forEach((obj) => canvas.add(obj));
6966
6995
  canvas.renderAll();
6967
6996
  };
6997
+ const changeZOrder = (canvas, elementId, direction) => {
6998
+ var _a, _b;
6999
+ if (!canvas) return null;
7000
+ const objects = canvas.getObjects();
7001
+ const sorted = [...objects].sort((a2, b2) => (a2.zIndex || 0) - (b2.zIndex || 0));
7002
+ const idx = sorted.findIndex((obj2) => {
7003
+ var _a2;
7004
+ return ((_a2 = obj2.get) == null ? void 0 : _a2.call(obj2, "id")) === elementId;
7005
+ });
7006
+ if (idx < 0) return null;
7007
+ const minZ = ((_a = sorted[0]) == null ? void 0 : _a.zIndex) ?? 0;
7008
+ const maxZ = ((_b = sorted[sorted.length - 1]) == null ? void 0 : _b.zIndex) ?? 0;
7009
+ const obj = sorted[idx];
7010
+ if (direction === "front") {
7011
+ obj.set("zIndex", maxZ + 1);
7012
+ reorderElementsByZIndex(canvas);
7013
+ return maxZ + 1;
7014
+ }
7015
+ if (direction === "back") {
7016
+ obj.set("zIndex", minZ - 1);
7017
+ reorderElementsByZIndex(canvas);
7018
+ return minZ - 1;
7019
+ }
7020
+ if (direction === "forward" && idx < sorted.length - 1) {
7021
+ const next = sorted[idx + 1];
7022
+ const myZ = obj.zIndex ?? idx;
7023
+ const nextZ = next.zIndex ?? idx + 1;
7024
+ obj.set("zIndex", nextZ);
7025
+ next.set("zIndex", myZ);
7026
+ reorderElementsByZIndex(canvas);
7027
+ return nextZ;
7028
+ }
7029
+ if (direction === "backward" && idx > 0) {
7030
+ const prev = sorted[idx - 1];
7031
+ const myZ = obj.zIndex ?? idx;
7032
+ const prevZ = prev.zIndex ?? idx - 1;
7033
+ obj.set("zIndex", prevZ);
7034
+ prev.set("zIndex", myZ);
7035
+ reorderElementsByZIndex(canvas);
7036
+ return prevZ;
7037
+ }
7038
+ return obj.zIndex ?? idx;
7039
+ };
6968
7040
  const getCanvasContext = (canvas) => {
6969
7041
  var _a, _b, _c, _d;
6970
7042
  if (!canvas || !((_b = (_a = canvas.elements) == null ? void 0 : _a.lower) == null ? void 0 : _b.ctx)) return;
@@ -6985,12 +7057,31 @@ const convertToCanvasPosition = (x2, y2, canvasMetadata) => {
6985
7057
  y: y2 * canvasMetadata.scaleY + canvasMetadata.height / 2
6986
7058
  };
6987
7059
  };
7060
+ const getObjectCanvasCenter = (obj) => {
7061
+ if (obj.getCenterPoint) {
7062
+ const p2 = obj.getCenterPoint();
7063
+ return { x: p2.x, y: p2.y };
7064
+ }
7065
+ return { x: obj.left ?? 0, y: obj.top ?? 0 };
7066
+ };
7067
+ const getObjectCanvasAngle = (obj) => {
7068
+ if (typeof obj.getTotalAngle === "function") {
7069
+ return obj.getTotalAngle();
7070
+ }
7071
+ return obj.angle ?? 0;
7072
+ };
6988
7073
  const convertToVideoPosition = (x2, y2, canvasMetadata, videoSize) => {
6989
7074
  return {
6990
7075
  x: Number((x2 / canvasMetadata.scaleX - videoSize.width / 2).toFixed(2)),
6991
7076
  y: Number((y2 / canvasMetadata.scaleY - videoSize.height / 2).toFixed(2))
6992
7077
  };
6993
7078
  };
7079
+ const convertToVideoDimensions = (widthCanvas, heightCanvas, canvasMetadata) => {
7080
+ return {
7081
+ width: Number((widthCanvas / canvasMetadata.scaleX).toFixed(2)),
7082
+ height: Number((heightCanvas / canvasMetadata.scaleY).toFixed(2))
7083
+ };
7084
+ };
6994
7085
  const getCurrentFrameEffect = (item, seekTime) => {
6995
7086
  var _a;
6996
7087
  let currentFrameEffect;
@@ -7002,6 +7093,17 @@ const getCurrentFrameEffect = (item, seekTime) => {
7002
7093
  }
7003
7094
  return currentFrameEffect;
7004
7095
  };
7096
+ const hexToRgba = (hex2, alpha2) => {
7097
+ const color2 = hex2.replace(/^#/, "");
7098
+ const fullHex = color2.length === 3 ? color2.split("").map((c2) => c2 + c2).join("") : color2;
7099
+ if (fullHex.length !== 6) {
7100
+ return hex2;
7101
+ }
7102
+ const r2 = parseInt(fullHex.slice(0, 2), 16);
7103
+ const g2 = parseInt(fullHex.slice(2, 4), 16);
7104
+ const b2 = parseInt(fullHex.slice(4, 6), 16);
7105
+ return `rgba(${r2}, ${g2}, ${b2}, ${alpha2})`;
7106
+ };
7005
7107
  const disabledControl = new ai({
7006
7108
  /** X position offset */
7007
7109
  x: 0,
@@ -7433,40 +7535,66 @@ const addTextElement = ({
7433
7535
  canvas,
7434
7536
  canvasMetadata
7435
7537
  }) => {
7436
- 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;
7538
+ 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;
7437
7539
  const { x: x2, y: y2 } = convertToCanvasPosition(
7438
7540
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
7439
7541
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
7440
7542
  canvasMetadata
7441
7543
  );
7442
- let width = ((_c = element.props) == null ? void 0 : _c.width) ? element.props.width * canvasMetadata.scaleX : canvasMetadata.width - 2 * MARGIN;
7443
- if ((_d = element.props) == null ? void 0 : _d.maxWidth) {
7444
- width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
7445
- }
7446
- const text = new Uo(((_e2 = element.props) == null ? void 0 : _e2.text) || element.t || "", {
7544
+ const fontSize = Math.floor(
7545
+ (((_c = element.props) == null ? void 0 : _c.fontSize) || DEFAULT_TEXT_PROPS.size) * canvasMetadata.scaleX
7546
+ );
7547
+ const fontFamily = ((_d = element.props) == null ? void 0 : _d.fontFamily) || DEFAULT_TEXT_PROPS.family;
7548
+ const fontStyle = ((_e2 = element.props) == null ? void 0 : _e2.fontStyle) || "normal";
7549
+ const fontWeight = ((_f = element.props) == null ? void 0 : _f.fontWeight) || "normal";
7550
+ let width;
7551
+ if (((_g = element.props) == null ? void 0 : _g.width) != null && element.props.width > 0) {
7552
+ width = element.props.width * canvasMetadata.scaleX;
7553
+ if ((_h2 = element.props) == null ? void 0 : _h2.maxWidth) {
7554
+ width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
7555
+ }
7556
+ } else {
7557
+ const textContent = ((_i2 = element.props) == null ? void 0 : _i2.text) ?? element.t ?? "";
7558
+ width = measureTextWidth(textContent, {
7559
+ fontSize,
7560
+ fontFamily,
7561
+ fontStyle,
7562
+ fontWeight
7563
+ });
7564
+ const padding = 4;
7565
+ width = width + padding * 2;
7566
+ if ((_j = element.props) == null ? void 0 : _j.maxWidth) {
7567
+ width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
7568
+ }
7569
+ if (width <= 0) width = 100;
7570
+ }
7571
+ const backgroundColor = ((_k = element.props) == null ? void 0 : _k.backgroundColor) ? hexToRgba(
7572
+ element.props.backgroundColor,
7573
+ ((_l = element.props) == null ? void 0 : _l.backgroundOpacity) ?? 1
7574
+ ) : void 0;
7575
+ const text = new Uo(((_m = element.props) == null ? void 0 : _m.text) || element.t || "", {
7447
7576
  left: x2,
7448
7577
  top: y2,
7449
7578
  originX: "center",
7450
7579
  originY: "center",
7451
- angle: ((_f = element.props) == null ? void 0 : _f.rotation) || 0,
7452
- fontSize: Math.floor(
7453
- (((_g = element.props) == null ? void 0 : _g.fontSize) || DEFAULT_TEXT_PROPS.size) * canvasMetadata.scaleX
7454
- ),
7455
- fontFamily: ((_h2 = element.props) == null ? void 0 : _h2.fontFamily) || DEFAULT_TEXT_PROPS.family,
7456
- fontStyle: ((_i2 = element.props) == null ? void 0 : _i2.fontStyle) || "normal",
7457
- fontWeight: ((_j = element.props) == null ? void 0 : _j.fontWeight) || "normal",
7458
- fill: ((_k = element.props) == null ? void 0 : _k.fill) || DEFAULT_TEXT_PROPS.fill,
7459
- opacity: ((_l = element.props) == null ? void 0 : _l.opacity) ?? 1,
7580
+ angle: ((_n2 = element.props) == null ? void 0 : _n2.rotation) || 0,
7581
+ fontSize,
7582
+ fontFamily,
7583
+ fontStyle,
7584
+ fontWeight,
7585
+ fill: ((_o2 = element.props) == null ? void 0 : _o2.fill) || DEFAULT_TEXT_PROPS.fill,
7586
+ opacity: ((_p = element.props) == null ? void 0 : _p.opacity) ?? 1,
7460
7587
  width,
7461
7588
  splitByGrapheme: false,
7462
- textAlign: ((_m = element.props) == null ? void 0 : _m.textAlign) || "center",
7463
- stroke: ((_n2 = element.props) == null ? void 0 : _n2.stroke) || DEFAULT_TEXT_PROPS.stroke,
7464
- strokeWidth: ((_o2 = element.props) == null ? void 0 : _o2.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
7465
- shadow: ((_p = element.props) == null ? void 0 : _p.shadowColor) ? new Ds({
7466
- offsetX: ((_r2 = (_q = element.props) == null ? void 0 : _q.shadowOffset) == null ? void 0 : _r2.length) && ((_t2 = (_s2 = element.props) == null ? void 0 : _s2.shadowOffset) == null ? void 0 : _t2.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
7467
- offsetY: ((_v = (_u = element.props) == null ? void 0 : _u.shadowOffset) == null ? void 0 : _v.length) && ((_w = element.props) == null ? void 0 : _w.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
7468
- blur: (((_x = element.props) == null ? void 0 : _x.shadowBlur) || 2) / 2,
7469
- color: (_y = element.props) == null ? void 0 : _y.shadowColor
7589
+ textAlign: ((_q = element.props) == null ? void 0 : _q.textAlign) || "center",
7590
+ stroke: ((_r2 = element.props) == null ? void 0 : _r2.stroke) || DEFAULT_TEXT_PROPS.stroke,
7591
+ strokeWidth: ((_s2 = element.props) == null ? void 0 : _s2.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
7592
+ ...backgroundColor && { backgroundColor },
7593
+ shadow: ((_t2 = element.props) == null ? void 0 : _t2.shadowColor) ? new Ds({
7594
+ offsetX: ((_v = (_u = element.props) == null ? void 0 : _u.shadowOffset) == null ? void 0 : _v.length) && ((_x = (_w = element.props) == null ? void 0 : _w.shadowOffset) == null ? void 0 : _x.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
7595
+ offsetY: ((_z = (_y = element.props) == null ? void 0 : _y.shadowOffset) == null ? void 0 : _z.length) && ((_A = element.props) == null ? void 0 : _A.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
7596
+ blur: (((_B = element.props) == null ? void 0 : _B.shadowBlur) || 2) / 2,
7597
+ color: (_C = element.props) == null ? void 0 : _C.shadowColor
7470
7598
  }) : void 0
7471
7599
  });
7472
7600
  text.set("id", element.id);
@@ -7487,7 +7615,8 @@ const setImageProps = ({
7487
7615
  img,
7488
7616
  element,
7489
7617
  index,
7490
- canvasMetadata
7618
+ canvasMetadata,
7619
+ lockAspectRatio = true
7491
7620
  }) => {
7492
7621
  var _a, _b, _c, _d, _e2;
7493
7622
  const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
@@ -7507,13 +7636,15 @@ const setImageProps = ({
7507
7636
  img.set("selectable", true);
7508
7637
  img.set("hasControls", true);
7509
7638
  img.set("touchAction", "all");
7639
+ img.set("lockUniScaling", lockAspectRatio);
7510
7640
  };
7511
7641
  const addCaptionElement = ({
7512
7642
  element,
7513
7643
  index,
7514
7644
  canvas,
7515
7645
  captionProps,
7516
- canvasMetadata
7646
+ canvasMetadata,
7647
+ lockAspectRatio = false
7517
7648
  }) => {
7518
7649
  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;
7519
7650
  const { x: x2, y: y2 } = convertToCanvasPosition(
@@ -7552,6 +7683,7 @@ const addCaptionElement = ({
7552
7683
  });
7553
7684
  caption.set("id", element.id);
7554
7685
  caption.set("zIndex", index);
7686
+ caption.set("lockUniScaling", lockAspectRatio);
7555
7687
  caption.controls.mt = disabledControl;
7556
7688
  caption.controls.mb = disabledControl;
7557
7689
  caption.controls.ml = disabledControl;
@@ -7599,7 +7731,8 @@ const addImageElement = async ({
7599
7731
  index,
7600
7732
  canvas,
7601
7733
  canvasMetadata,
7602
- currentFrameEffect
7734
+ currentFrameEffect,
7735
+ lockAspectRatio = true
7603
7736
  }) => {
7604
7737
  try {
7605
7738
  const img = await oa.fromURL(imageUrl || element.props.src || "");
@@ -7608,7 +7741,7 @@ const addImageElement = async ({
7608
7741
  originY: "center",
7609
7742
  lockMovementX: false,
7610
7743
  lockMovementY: false,
7611
- lockUniScaling: true,
7744
+ lockUniScaling: lockAspectRatio,
7612
7745
  hasControls: false,
7613
7746
  selectable: false
7614
7747
  });
@@ -7619,10 +7752,11 @@ const addImageElement = async ({
7619
7752
  index,
7620
7753
  canvas,
7621
7754
  canvasMetadata,
7622
- currentFrameEffect
7755
+ currentFrameEffect,
7756
+ lockAspectRatio
7623
7757
  });
7624
7758
  } else {
7625
- setImageProps({ img, element, index, canvasMetadata });
7759
+ setImageProps({ img, element, index, canvasMetadata, lockAspectRatio });
7626
7760
  canvas.add(img);
7627
7761
  return img;
7628
7762
  }
@@ -7635,7 +7769,8 @@ const addMediaGroup = ({
7635
7769
  index,
7636
7770
  canvas,
7637
7771
  canvasMetadata,
7638
- currentFrameEffect
7772
+ currentFrameEffect,
7773
+ lockAspectRatio = true
7639
7774
  }) => {
7640
7775
  var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2;
7641
7776
  let frameSize;
@@ -7724,6 +7859,7 @@ const addMediaGroup = ({
7724
7859
  group.controls.mtr = rotateControl;
7725
7860
  group.set("id", element.id);
7726
7861
  group.set("zIndex", index);
7862
+ group.set("lockUniScaling", lockAspectRatio);
7727
7863
  canvas.add(group);
7728
7864
  return group;
7729
7865
  };
@@ -7731,7 +7867,8 @@ const addRectElement = ({
7731
7867
  element,
7732
7868
  index,
7733
7869
  canvas,
7734
- canvasMetadata
7870
+ canvasMetadata,
7871
+ lockAspectRatio = false
7735
7872
  }) => {
7736
7873
  var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k;
7737
7874
  const { x: x2, y: y2 } = convertToCanvasPosition(
@@ -7769,6 +7906,7 @@ const addRectElement = ({
7769
7906
  });
7770
7907
  rect.set("id", element.id);
7771
7908
  rect.set("zIndex", index);
7909
+ rect.set("lockUniScaling", lockAspectRatio);
7772
7910
  rect.controls.mtr = rotateControl;
7773
7911
  canvas.add(rect);
7774
7912
  return rect;
@@ -7777,9 +7915,10 @@ const addCircleElement = ({
7777
7915
  element,
7778
7916
  index,
7779
7917
  canvas,
7780
- canvasMetadata
7918
+ canvasMetadata,
7919
+ lockAspectRatio = true
7781
7920
  }) => {
7782
- var _a, _b, _c, _d, _e2, _f;
7921
+ var _a, _b, _c, _d, _e2, _f, _g;
7783
7922
  const { x: x2, y: y2 } = convertToCanvasPosition(
7784
7923
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
7785
7924
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
@@ -7795,7 +7934,9 @@ const addCircleElement = ({
7795
7934
  stroke: ((_e2 = element.props) == null ? void 0 : _e2.stroke) || "#000000",
7796
7935
  strokeWidth: (((_f = element.props) == null ? void 0 : _f.lineWidth) || 0) * canvasMetadata.scaleX,
7797
7936
  originX: "center",
7798
- originY: "center"
7937
+ originY: "center",
7938
+ // Respect element opacity (0–1). Defaults to fully opaque.
7939
+ opacity: ((_g = element.props) == null ? void 0 : _g.opacity) ?? 1
7799
7940
  });
7800
7941
  circle.controls.mt = disabledControl;
7801
7942
  circle.controls.mb = disabledControl;
@@ -7804,6 +7945,7 @@ const addCircleElement = ({
7804
7945
  circle.controls.mtr = disabledControl;
7805
7946
  circle.set("id", element.id);
7806
7947
  circle.set("zIndex", index);
7948
+ circle.set("lockUniScaling", lockAspectRatio);
7807
7949
  canvas.add(circle);
7808
7950
  return circle;
7809
7951
  };
@@ -7838,17 +7980,462 @@ const addBackgroundColor = ({
7838
7980
  canvas.add(bgRect);
7839
7981
  return bgRect;
7840
7982
  };
7983
+ const VideoElement = {
7984
+ name: ELEMENT_TYPES.VIDEO,
7985
+ async add(params) {
7986
+ var _a, _b;
7987
+ const {
7988
+ element,
7989
+ index,
7990
+ canvas,
7991
+ canvasMetadata,
7992
+ seekTime = 0,
7993
+ elementFrameMapRef,
7994
+ getCurrentFrameEffect: getFrameEffect
7995
+ } = params;
7996
+ if (!getFrameEffect || !elementFrameMapRef) return;
7997
+ const currentFrameEffect = getFrameEffect(element, seekTime);
7998
+ elementFrameMapRef.current[element.id] = currentFrameEffect;
7999
+ const snapTime = (seekTime - ((element == null ? void 0 : element.s) ?? 0)) * (((_a = element == null ? void 0 : element.props) == null ? void 0 : _a.playbackRate) ?? 1) + (((_b = element == null ? void 0 : element.props) == null ? void 0 : _b.time) ?? 0);
8000
+ await addVideoElement({
8001
+ element,
8002
+ index,
8003
+ canvas,
8004
+ canvasMetadata,
8005
+ currentFrameEffect,
8006
+ snapTime
8007
+ });
8008
+ if (element.timelineType === "scene") {
8009
+ await addBackgroundColor({
8010
+ element,
8011
+ index,
8012
+ canvas,
8013
+ canvasMetadata
8014
+ });
8015
+ }
8016
+ },
8017
+ updateFromFabricObject(object, element, context) {
8018
+ const canvasCenter = getObjectCanvasCenter(object);
8019
+ const { x: x2, y: y2 } = convertToVideoPosition(
8020
+ canvasCenter.x,
8021
+ canvasCenter.y,
8022
+ context.canvasMetadata,
8023
+ context.videoSize
8024
+ );
8025
+ const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
8026
+ const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
8027
+ const { width: fw, height: fh2 } = convertToVideoDimensions(
8028
+ scaledW,
8029
+ scaledH,
8030
+ context.canvasMetadata
8031
+ );
8032
+ const updatedFrameSize = [fw, fh2];
8033
+ const currentFrameEffect = context.elementFrameMapRef.current[element.id];
8034
+ if (currentFrameEffect) {
8035
+ context.elementFrameMapRef.current[element.id] = {
8036
+ ...currentFrameEffect,
8037
+ props: {
8038
+ ...currentFrameEffect.props,
8039
+ framePosition: { x: x2, y: y2 },
8040
+ frameSize: updatedFrameSize
8041
+ }
8042
+ };
8043
+ return {
8044
+ element: {
8045
+ ...element,
8046
+ frameEffects: (element.frameEffects || []).map(
8047
+ (fe2) => fe2.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
8048
+ ...fe2,
8049
+ props: {
8050
+ ...fe2.props,
8051
+ framePosition: { x: x2, y: y2 },
8052
+ frameSize: updatedFrameSize
8053
+ }
8054
+ } : fe2
8055
+ )
8056
+ }
8057
+ };
8058
+ }
8059
+ const frame2 = element.frame;
8060
+ return {
8061
+ element: {
8062
+ ...element,
8063
+ frame: {
8064
+ ...frame2,
8065
+ rotation: getObjectCanvasAngle(object),
8066
+ size: updatedFrameSize,
8067
+ x: x2,
8068
+ y: y2
8069
+ }
8070
+ }
8071
+ };
8072
+ }
8073
+ };
8074
+ const ImageElement = {
8075
+ name: ELEMENT_TYPES.IMAGE,
8076
+ async add(params) {
8077
+ var _a;
8078
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
8079
+ await addImageElement({
8080
+ element,
8081
+ index,
8082
+ canvas,
8083
+ canvasMetadata,
8084
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8085
+ });
8086
+ if (element.timelineType === "scene") {
8087
+ await addBackgroundColor({
8088
+ element,
8089
+ index,
8090
+ canvas,
8091
+ canvasMetadata
8092
+ });
8093
+ }
8094
+ },
8095
+ updateFromFabricObject(object, element, context) {
8096
+ const canvasCenter = getObjectCanvasCenter(object);
8097
+ const { x: x2, y: y2 } = convertToVideoPosition(
8098
+ canvasCenter.x,
8099
+ canvasCenter.y,
8100
+ context.canvasMetadata,
8101
+ context.videoSize
8102
+ );
8103
+ const currentFrameEffect = context.elementFrameMapRef.current[element.id];
8104
+ if (object.type === "group") {
8105
+ const scaledW2 = (object.width ?? 0) * (object.scaleX ?? 1);
8106
+ const scaledH2 = (object.height ?? 0) * (object.scaleY ?? 1);
8107
+ const { width: fw, height: fh2 } = convertToVideoDimensions(
8108
+ scaledW2,
8109
+ scaledH2,
8110
+ context.canvasMetadata
8111
+ );
8112
+ const updatedFrameSize = [fw, fh2];
8113
+ if (currentFrameEffect) {
8114
+ context.elementFrameMapRef.current[element.id] = {
8115
+ ...currentFrameEffect,
8116
+ props: {
8117
+ ...currentFrameEffect.props,
8118
+ framePosition: { x: x2, y: y2 },
8119
+ frameSize: updatedFrameSize
8120
+ }
8121
+ };
8122
+ return {
8123
+ element: {
8124
+ ...element,
8125
+ // Keep the base frame in sync with the active frame effect
8126
+ // so visualizer `Rect {...element.frame}` reflects the same size/position.
8127
+ frame: element.frame ? {
8128
+ ...element.frame,
8129
+ rotation: getObjectCanvasAngle(object),
8130
+ size: updatedFrameSize,
8131
+ x: x2,
8132
+ y: y2
8133
+ } : element.frame,
8134
+ frameEffects: (element.frameEffects || []).map(
8135
+ (fe2) => fe2.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
8136
+ ...fe2,
8137
+ props: {
8138
+ ...fe2.props,
8139
+ framePosition: { x: x2, y: y2 },
8140
+ frameSize: updatedFrameSize
8141
+ }
8142
+ } : fe2
8143
+ )
8144
+ }
8145
+ };
8146
+ }
8147
+ const frame2 = element.frame;
8148
+ return {
8149
+ element: {
8150
+ ...element,
8151
+ frame: {
8152
+ ...frame2,
8153
+ rotation: getObjectCanvasAngle(object),
8154
+ size: updatedFrameSize,
8155
+ x: x2,
8156
+ y: y2
8157
+ }
8158
+ }
8159
+ };
8160
+ }
8161
+ const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
8162
+ const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
8163
+ const { width, height } = convertToVideoDimensions(
8164
+ scaledW,
8165
+ scaledH,
8166
+ context.canvasMetadata
8167
+ );
8168
+ return {
8169
+ element: {
8170
+ ...element,
8171
+ props: {
8172
+ ...element.props,
8173
+ rotation: getObjectCanvasAngle(object),
8174
+ width,
8175
+ height,
8176
+ x: x2,
8177
+ y: y2
8178
+ }
8179
+ }
8180
+ };
8181
+ }
8182
+ };
8183
+ const RectElement = {
8184
+ name: ELEMENT_TYPES.RECT,
8185
+ async add(params) {
8186
+ var _a;
8187
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
8188
+ await addRectElement({
8189
+ element,
8190
+ index,
8191
+ canvas,
8192
+ canvasMetadata,
8193
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8194
+ });
8195
+ },
8196
+ updateFromFabricObject(object, element, context) {
8197
+ var _a, _b;
8198
+ const canvasCenter = getObjectCanvasCenter(object);
8199
+ const { x: x2, y: y2 } = convertToVideoPosition(
8200
+ canvasCenter.x,
8201
+ canvasCenter.y,
8202
+ context.canvasMetadata,
8203
+ context.videoSize
8204
+ );
8205
+ return {
8206
+ element: {
8207
+ ...element,
8208
+ props: {
8209
+ ...element.props,
8210
+ rotation: getObjectCanvasAngle(object),
8211
+ width: (((_a = element.props) == null ? void 0 : _a.width) ?? 0) * object.scaleX,
8212
+ height: (((_b = element.props) == null ? void 0 : _b.height) ?? 0) * object.scaleY,
8213
+ x: x2,
8214
+ y: y2
8215
+ }
8216
+ }
8217
+ };
8218
+ }
8219
+ };
8220
+ const CircleElement = {
8221
+ name: ELEMENT_TYPES.CIRCLE,
8222
+ async add(params) {
8223
+ var _a;
8224
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
8225
+ await addCircleElement({
8226
+ element,
8227
+ index,
8228
+ canvas,
8229
+ canvasMetadata,
8230
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8231
+ });
8232
+ },
8233
+ updateFromFabricObject(object, element, context) {
8234
+ var _a, _b;
8235
+ const canvasCenter = getObjectCanvasCenter(object);
8236
+ const { x: x2, y: y2 } = convertToVideoPosition(
8237
+ canvasCenter.x,
8238
+ canvasCenter.y,
8239
+ context.canvasMetadata,
8240
+ context.videoSize
8241
+ );
8242
+ const radius = Number(
8243
+ ((((_a = element.props) == null ? void 0 : _a.radius) ?? 0) * object.scaleX).toFixed(2)
8244
+ );
8245
+ const opacity = object.opacity != null ? object.opacity : (_b = element.props) == null ? void 0 : _b.opacity;
8246
+ return {
8247
+ element: {
8248
+ ...element,
8249
+ props: {
8250
+ ...element.props,
8251
+ rotation: getObjectCanvasAngle(object),
8252
+ radius,
8253
+ height: radius * 2,
8254
+ width: radius * 2,
8255
+ x: x2,
8256
+ y: y2,
8257
+ ...opacity != null && { opacity }
8258
+ }
8259
+ }
8260
+ };
8261
+ }
8262
+ };
8263
+ const TextElement = {
8264
+ name: ELEMENT_TYPES.TEXT,
8265
+ async add(params) {
8266
+ const { element, index, canvas, canvasMetadata } = params;
8267
+ await addTextElement({
8268
+ element,
8269
+ index,
8270
+ canvas,
8271
+ canvasMetadata
8272
+ });
8273
+ },
8274
+ updateFromFabricObject(object, element, context) {
8275
+ const canvasCenter = getObjectCanvasCenter(object);
8276
+ const { x: x2, y: y2 } = convertToVideoPosition(
8277
+ canvasCenter.x,
8278
+ canvasCenter.y,
8279
+ context.canvasMetadata,
8280
+ context.videoSize
8281
+ );
8282
+ return {
8283
+ element: {
8284
+ ...element,
8285
+ props: {
8286
+ ...element.props,
8287
+ rotation: getObjectCanvasAngle(object),
8288
+ x: x2,
8289
+ y: y2
8290
+ }
8291
+ }
8292
+ };
8293
+ }
8294
+ };
8295
+ const CaptionElement = {
8296
+ name: ELEMENT_TYPES.CAPTION,
8297
+ async add(params) {
8298
+ var _a;
8299
+ const { element, index, canvas, captionProps, canvasMetadata, lockAspectRatio } = params;
8300
+ await addCaptionElement({
8301
+ element,
8302
+ index,
8303
+ canvas,
8304
+ captionProps: captionProps ?? {},
8305
+ canvasMetadata,
8306
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8307
+ });
8308
+ },
8309
+ updateFromFabricObject(object, element, context) {
8310
+ var _a;
8311
+ const canvasCenter = getObjectCanvasCenter(object);
8312
+ const { x: x2, y: y2 } = convertToVideoPosition(
8313
+ canvasCenter.x,
8314
+ canvasCenter.y,
8315
+ context.canvasMetadata,
8316
+ context.videoSize
8317
+ );
8318
+ if ((_a = context.captionPropsRef.current) == null ? void 0 : _a.applyToAll) {
8319
+ return {
8320
+ element,
8321
+ operation: CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED,
8322
+ payload: {
8323
+ element,
8324
+ props: {
8325
+ ...context.captionPropsRef.current,
8326
+ x: x2,
8327
+ y: y2
8328
+ }
8329
+ }
8330
+ };
8331
+ }
8332
+ return {
8333
+ element: {
8334
+ ...element,
8335
+ props: {
8336
+ ...element.props,
8337
+ x: x2,
8338
+ y: y2
8339
+ }
8340
+ }
8341
+ };
8342
+ }
8343
+ };
8344
+ const WatermarkElement = {
8345
+ name: "watermark",
8346
+ async add(params) {
8347
+ const { element, index, canvas, canvasMetadata, watermarkPropsRef } = params;
8348
+ if (element.type === ELEMENT_TYPES.TEXT) {
8349
+ if (watermarkPropsRef) watermarkPropsRef.current = element.props;
8350
+ await addTextElement({
8351
+ element,
8352
+ index,
8353
+ canvas,
8354
+ canvasMetadata
8355
+ });
8356
+ } else if (element.type === ELEMENT_TYPES.IMAGE) {
8357
+ await addImageElement({
8358
+ element,
8359
+ index,
8360
+ canvas,
8361
+ canvasMetadata
8362
+ });
8363
+ }
8364
+ },
8365
+ updateFromFabricObject(object, element, context) {
8366
+ const { x: x2, y: y2 } = convertToVideoPosition(
8367
+ object.left,
8368
+ object.top,
8369
+ context.canvasMetadata,
8370
+ context.videoSize
8371
+ );
8372
+ const rotation = object.angle != null ? object.angle : void 0;
8373
+ const opacity = object.opacity != null ? object.opacity : void 0;
8374
+ const baseProps = element.type === ELEMENT_TYPES.TEXT ? context.watermarkPropsRef.current ?? element.props ?? {} : { ...element.props };
8375
+ const props = element.type === ELEMENT_TYPES.IMAGE && (object.scaleX != null || object.scaleY != null) ? {
8376
+ ...baseProps,
8377
+ width: baseProps.width != null && object.scaleX != null ? baseProps.width * object.scaleX : baseProps.width,
8378
+ height: baseProps.height != null && object.scaleY != null ? baseProps.height * object.scaleY : baseProps.height
8379
+ } : baseProps;
8380
+ const payload = {
8381
+ position: { x: x2, y: y2 },
8382
+ ...rotation != null && { rotation },
8383
+ ...opacity != null && { opacity },
8384
+ ...Object.keys(props).length > 0 && { props }
8385
+ };
8386
+ return {
8387
+ element: { ...element, props: { ...element.props, x: x2, y: y2, rotation, opacity, ...props } },
8388
+ operation: CANVAS_OPERATIONS.WATERMARK_UPDATED,
8389
+ payload
8390
+ };
8391
+ }
8392
+ };
8393
+ class ElementController {
8394
+ constructor() {
8395
+ __publicField2(this, "elements", /* @__PURE__ */ new Map());
8396
+ }
8397
+ register(handler) {
8398
+ this.elements.set(handler.name, handler);
8399
+ }
8400
+ get(name) {
8401
+ return this.elements.get(name);
8402
+ }
8403
+ list() {
8404
+ return Array.from(this.elements.keys());
8405
+ }
8406
+ }
8407
+ const elementController = new ElementController();
8408
+ function registerElements() {
8409
+ elementController.register(VideoElement);
8410
+ elementController.register(ImageElement);
8411
+ elementController.register(RectElement);
8412
+ elementController.register(CircleElement);
8413
+ elementController.register(TextElement);
8414
+ elementController.register(CaptionElement);
8415
+ elementController.register(WatermarkElement);
8416
+ }
8417
+ registerElements();
7841
8418
  const useTwickCanvas = ({
7842
8419
  onCanvasReady,
7843
- onCanvasOperation
8420
+ onCanvasOperation,
8421
+ /**
8422
+ * When true, holding Shift while dragging an object will lock movement to
8423
+ * the dominant axis (horizontal or vertical). This mirrors behavior in
8424
+ * professional editors and improves precise alignment.
8425
+ *
8426
+ * Default: false (opt‑in to avoid surprising existing consumers).
8427
+ */
8428
+ enableShiftAxisLock = false
7844
8429
  }) => {
7845
8430
  const [twickCanvas, setTwickCanvas] = React.useState(null);
7846
8431
  const elementMap = React.useRef({});
8432
+ const watermarkPropsRef = React.useRef(null);
7847
8433
  const elementFrameMap = React.useRef({});
7848
8434
  const twickCanvasRef = React.useRef(null);
7849
8435
  const videoSizeRef = React.useRef({ width: 1, height: 1 });
7850
8436
  const canvasResolutionRef = React.useRef({ width: 1, height: 1 });
7851
8437
  const captionPropsRef = React.useRef(null);
8438
+ const axisLockStateRef = React.useRef(null);
7852
8439
  const canvasMetadataRef = React.useRef({
7853
8440
  width: 0,
7854
8441
  height: 0,
@@ -7863,6 +8450,57 @@ const useTwickCanvas = ({
7863
8450
  canvasMetadataRef.current.scaleY = canvasMetadataRef.current.height / videoSize.height;
7864
8451
  }
7865
8452
  };
8453
+ const handleObjectMoving = (event) => {
8454
+ var _a;
8455
+ if (!enableShiftAxisLock) return;
8456
+ const target = event == null ? void 0 : event.target;
8457
+ const transform = event == null ? void 0 : event.transform;
8458
+ const pointerEvent = event == null ? void 0 : event.e;
8459
+ if (!target || !transform || !pointerEvent) {
8460
+ axisLockStateRef.current = null;
8461
+ return;
8462
+ }
8463
+ if (!pointerEvent.shiftKey) {
8464
+ axisLockStateRef.current = null;
8465
+ return;
8466
+ }
8467
+ const original = transform.original;
8468
+ if (!original || typeof target.left !== "number" || typeof target.top !== "number") {
8469
+ axisLockStateRef.current = null;
8470
+ return;
8471
+ }
8472
+ if (!axisLockStateRef.current) {
8473
+ const dx = Math.abs(target.left - original.left);
8474
+ const dy = Math.abs(target.top - original.top);
8475
+ axisLockStateRef.current = {
8476
+ axis: dx >= dy ? "x" : "y"
8477
+ };
8478
+ }
8479
+ if (axisLockStateRef.current.axis === "x") {
8480
+ target.top = original.top;
8481
+ } else {
8482
+ target.left = original.left;
8483
+ }
8484
+ (_a = target.canvas) == null ? void 0 : _a.requestRenderAll();
8485
+ };
8486
+ const applyMarqueeSelectionControls = () => {
8487
+ const canvasInstance = twickCanvasRef.current;
8488
+ if (!canvasInstance) return;
8489
+ const activeObject = canvasInstance.getActiveObject();
8490
+ if (!activeObject) return;
8491
+ if (activeObject instanceof Qo) {
8492
+ activeObject.controls.mt = disabledControl;
8493
+ activeObject.controls.mb = disabledControl;
8494
+ activeObject.controls.ml = disabledControl;
8495
+ activeObject.controls.mr = disabledControl;
8496
+ activeObject.controls.bl = disabledControl;
8497
+ activeObject.controls.br = disabledControl;
8498
+ activeObject.controls.tl = disabledControl;
8499
+ activeObject.controls.tr = disabledControl;
8500
+ activeObject.controls.mtr = rotateControl;
8501
+ canvasInstance.requestRenderAll();
8502
+ }
8503
+ };
7866
8504
  const buildCanvas = ({
7867
8505
  videoSize,
7868
8506
  canvasSize,
@@ -7882,6 +8520,9 @@ const useTwickCanvas = ({
7882
8520
  if (twickCanvasRef.current) {
7883
8521
  twickCanvasRef.current.off("mouse:up", handleMouseUp);
7884
8522
  twickCanvasRef.current.off("text:editing:exited", onTextEdit);
8523
+ twickCanvasRef.current.off("object:moving", handleObjectMoving);
8524
+ twickCanvasRef.current.off("selection:created", applyMarqueeSelectionControls);
8525
+ twickCanvasRef.current.off("selection:updated", applyMarqueeSelectionControls);
7885
8526
  twickCanvasRef.current.dispose();
7886
8527
  }
7887
8528
  const { canvas, canvasMetadata } = createCanvas({
@@ -7899,6 +8540,9 @@ const useTwickCanvas = ({
7899
8540
  videoSizeRef.current = videoSize;
7900
8541
  canvas == null ? void 0 : canvas.on("mouse:up", handleMouseUp);
7901
8542
  canvas == null ? void 0 : canvas.on("text:editing:exited", onTextEdit);
8543
+ canvas == null ? void 0 : canvas.on("object:moving", handleObjectMoving);
8544
+ canvas == null ? void 0 : canvas.on("selection:created", applyMarqueeSelectionControls);
8545
+ canvas == null ? void 0 : canvas.on("selection:updated", applyMarqueeSelectionControls);
7902
8546
  canvasResolutionRef.current = canvasSize;
7903
8547
  setTwickCanvas(canvas);
7904
8548
  twickCanvasRef.current = canvas;
@@ -7925,7 +8569,8 @@ const useTwickCanvas = ({
7925
8569
  if (event.target) {
7926
8570
  const object = event.target;
7927
8571
  const elementId = object.get("id");
7928
- if (((_a = event.transform) == null ? void 0 : _a.action) === "drag") {
8572
+ const action = (_a = event.transform) == null ? void 0 : _a.action;
8573
+ if (action === "drag") {
7929
8574
  const original = event.transform.original;
7930
8575
  if (object.left === original.left && object.top === original.top) {
7931
8576
  onCanvasOperation == null ? void 0 : onCanvasOperation(
@@ -7935,149 +8580,67 @@ const useTwickCanvas = ({
7935
8580
  return;
7936
8581
  }
7937
8582
  }
7938
- switch ((_b = event.transform) == null ? void 0 : _b.action) {
8583
+ const context = {
8584
+ canvasMetadata: canvasMetadataRef.current,
8585
+ videoSize: videoSizeRef.current,
8586
+ elementFrameMapRef: elementFrameMap,
8587
+ captionPropsRef,
8588
+ watermarkPropsRef
8589
+ };
8590
+ if (object instanceof Qo && (action === "drag" || action === "rotate")) {
8591
+ const objects = object.getObjects();
8592
+ for (const fabricObj of objects) {
8593
+ const id2 = fabricObj.get("id");
8594
+ if (!id2 || id2 === "e-watermark") continue;
8595
+ const currentElement = elementMap.current[id2];
8596
+ if (!currentElement) continue;
8597
+ const handler = elementController.get(currentElement.type);
8598
+ const result = (_b = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _b.call(
8599
+ handler,
8600
+ fabricObj,
8601
+ currentElement,
8602
+ context
8603
+ );
8604
+ if (result) {
8605
+ elementMap.current[id2] = result.element;
8606
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
8607
+ result.operation ?? CANVAS_OPERATIONS.ITEM_UPDATED,
8608
+ result.payload ?? result.element
8609
+ );
8610
+ }
8611
+ }
8612
+ return;
8613
+ }
8614
+ switch (action) {
7939
8615
  case "drag":
7940
8616
  case "scale":
7941
8617
  case "scaleX":
7942
8618
  case "scaleY":
7943
- case "rotate":
7944
- const { x: x2, y: y2 } = convertToVideoPosition(
7945
- object.left,
7946
- object.top,
7947
- canvasMetadataRef.current,
7948
- videoSizeRef.current
8619
+ case "rotate": {
8620
+ const currentElement = elementMap.current[elementId];
8621
+ const handler = elementController.get(
8622
+ elementId === "e-watermark" ? "watermark" : currentElement == null ? void 0 : currentElement.type
7949
8623
  );
7950
- if (elementMap.current[elementId].type === "caption") {
7951
- if ((_c = captionPropsRef.current) == null ? void 0 : _c.applyToAll) {
7952
- onCanvasOperation == null ? void 0 : onCanvasOperation(CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED, {
7953
- element: elementMap.current[elementId],
7954
- props: {
7955
- ...captionPropsRef.current,
7956
- x: x2,
7957
- y: y2
7958
- }
7959
- });
7960
- } else {
7961
- elementMap.current[elementId] = {
7962
- ...elementMap.current[elementId],
7963
- props: {
7964
- ...elementMap.current[elementId].props,
7965
- x: x2,
7966
- y: y2
7967
- }
7968
- };
7969
- onCanvasOperation == null ? void 0 : onCanvasOperation(
7970
- CANVAS_OPERATIONS.ITEM_UPDATED,
7971
- elementMap.current[elementId]
7972
- );
7973
- }
7974
- } else {
7975
- if ((object == null ? void 0 : object.type) === "group") {
7976
- const currentFrameEffect = elementFrameMap.current[elementId];
7977
- let updatedFrameSize;
7978
- if (currentFrameEffect) {
7979
- updatedFrameSize = [
7980
- currentFrameEffect.props.frameSize[0] * object.scaleX,
7981
- currentFrameEffect.props.frameSize[1] * object.scaleY
7982
- ];
7983
- } else {
7984
- updatedFrameSize = [
7985
- elementMap.current[elementId].frame.size[0] * object.scaleX,
7986
- elementMap.current[elementId].frame.size[1] * object.scaleY
7987
- ];
7988
- }
7989
- if (currentFrameEffect) {
7990
- elementMap.current[elementId] = {
7991
- ...elementMap.current[elementId],
7992
- frameEffects: (elementMap.current[elementId].frameEffects || []).map(
7993
- (frameEffect) => frameEffect.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
7994
- ...frameEffect,
7995
- props: {
7996
- ...frameEffect.props,
7997
- framePosition: {
7998
- x: x2,
7999
- y: y2
8000
- },
8001
- frameSize: updatedFrameSize
8002
- }
8003
- } : frameEffect
8004
- )
8005
- };
8006
- elementFrameMap.current[elementId] = {
8007
- ...elementFrameMap.current[elementId],
8008
- framePosition: {
8009
- x: x2,
8010
- y: y2
8011
- },
8012
- frameSize: updatedFrameSize
8013
- };
8014
- } else {
8015
- elementMap.current[elementId] = {
8016
- ...elementMap.current[elementId],
8017
- frame: {
8018
- ...elementMap.current[elementId].frame,
8019
- rotation: object.angle,
8020
- size: updatedFrameSize,
8021
- x: x2,
8022
- y: y2
8023
- }
8024
- };
8025
- }
8026
- } else {
8027
- if ((object == null ? void 0 : object.type) === "text") {
8028
- elementMap.current[elementId] = {
8029
- ...elementMap.current[elementId],
8030
- props: {
8031
- ...elementMap.current[elementId].props,
8032
- rotation: object.angle,
8033
- x: x2,
8034
- y: y2
8035
- }
8036
- };
8037
- } else if ((object == null ? void 0 : object.type) === "circle") {
8038
- const radius = Number(
8039
- (elementMap.current[elementId].props.radius * object.scaleX).toFixed(2)
8040
- );
8041
- elementMap.current[elementId] = {
8042
- ...elementMap.current[elementId],
8043
- props: {
8044
- ...elementMap.current[elementId].props,
8045
- rotation: object.angle,
8046
- radius,
8047
- height: radius * 2,
8048
- width: radius * 2,
8049
- x: x2,
8050
- y: y2
8051
- }
8052
- };
8053
- } else {
8054
- elementMap.current[elementId] = {
8055
- ...elementMap.current[elementId],
8056
- props: {
8057
- ...elementMap.current[elementId].props,
8058
- rotation: object.angle,
8059
- width: elementMap.current[elementId].props.width * object.scaleX,
8060
- height: elementMap.current[elementId].props.height * object.scaleY,
8061
- x: x2,
8062
- y: y2
8063
- }
8064
- };
8065
- }
8066
- }
8624
+ const result = (_c = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _c.call(handler, object, currentElement ?? { id: elementId, type: "text", props: {} }, context);
8625
+ if (result) {
8626
+ elementMap.current[elementId] = result.element;
8067
8627
  onCanvasOperation == null ? void 0 : onCanvasOperation(
8068
- CANVAS_OPERATIONS.ITEM_UPDATED,
8069
- elementMap.current[elementId]
8628
+ result.operation ?? CANVAS_OPERATIONS.ITEM_UPDATED,
8629
+ result.payload ?? result.element
8070
8630
  );
8071
8631
  }
8072
8632
  break;
8633
+ }
8073
8634
  }
8074
8635
  }
8075
8636
  };
8076
8637
  const setCanvasElements = async ({
8077
8638
  elements,
8639
+ watermark,
8078
8640
  seekTime = 0,
8079
8641
  captionProps,
8080
- cleanAndAdd = false
8642
+ cleanAndAdd = false,
8643
+ lockAspectRatio
8081
8644
  }) => {
8082
8645
  if (!twickCanvas || !getCanvasContext(twickCanvas)) return;
8083
8646
  try {
@@ -8090,21 +8653,36 @@ const useTwickCanvas = ({
8090
8653
  }
8091
8654
  }
8092
8655
  captionPropsRef.current = captionProps;
8656
+ const uniqueElements = [];
8657
+ const seenIds = /* @__PURE__ */ new Set();
8658
+ for (const el of elements) {
8659
+ if (!el || !el.id) continue;
8660
+ if (seenIds.has(el.id)) continue;
8661
+ seenIds.add(el.id);
8662
+ uniqueElements.push(el);
8663
+ }
8093
8664
  await Promise.all(
8094
- elements.map(async (element, index) => {
8665
+ uniqueElements.map(async (element, index) => {
8095
8666
  try {
8096
8667
  if (!element) return;
8668
+ const zOrder = element.zIndex ?? index;
8097
8669
  await addElementToCanvas({
8098
8670
  element,
8099
- index,
8671
+ index: zOrder,
8100
8672
  reorder: false,
8101
8673
  seekTime,
8102
- captionProps
8674
+ captionProps,
8675
+ lockAspectRatio
8103
8676
  });
8104
8677
  } catch {
8105
8678
  }
8106
8679
  })
8107
8680
  );
8681
+ if (watermark) {
8682
+ addWatermarkToCanvas({
8683
+ element: watermark
8684
+ });
8685
+ }
8108
8686
  reorderElementsByZIndex(twickCanvas);
8109
8687
  } catch {
8110
8688
  }
@@ -8114,316 +8692,1033 @@ const useTwickCanvas = ({
8114
8692
  index,
8115
8693
  reorder = true,
8116
8694
  seekTime,
8117
- captionProps
8695
+ captionProps,
8696
+ lockAspectRatio
8118
8697
  }) => {
8119
- var _a, _b;
8698
+ var _a;
8120
8699
  if (!twickCanvas) return;
8121
- switch (element.type) {
8122
- case ELEMENT_TYPES.VIDEO:
8123
- const currentFrameEffect = getCurrentFrameEffect(
8124
- element,
8125
- seekTime || 0
8126
- );
8127
- elementFrameMap.current[element.id] = currentFrameEffect;
8128
- const snapTime = ((seekTime || 0) - ((element == null ? void 0 : element.s) || 0)) * (((_a = element == null ? void 0 : element.props) == null ? void 0 : _a.playbackRate) || 1) + (((_b = element == null ? void 0 : element.props) == null ? void 0 : _b.time) || 0);
8129
- await addVideoElement({
8130
- element,
8131
- index,
8132
- canvas: twickCanvas,
8133
- canvasMetadata: canvasMetadataRef.current,
8134
- currentFrameEffect,
8135
- snapTime
8136
- });
8137
- if (element.timelineType === "scene") {
8138
- await addBackgroundColor({
8139
- element,
8140
- index,
8141
- canvas: twickCanvas,
8142
- canvasMetadata: canvasMetadataRef.current
8143
- });
8144
- }
8145
- break;
8146
- case ELEMENT_TYPES.IMAGE:
8147
- await addImageElement({
8148
- element,
8149
- index,
8150
- canvas: twickCanvas,
8151
- canvasMetadata: canvasMetadataRef.current
8152
- });
8153
- if (element.timelineType === "scene") {
8154
- await addBackgroundColor({
8155
- element,
8156
- index,
8157
- canvas: twickCanvas,
8158
- canvasMetadata: canvasMetadataRef.current
8159
- });
8160
- }
8161
- break;
8162
- case ELEMENT_TYPES.RECT:
8163
- await addRectElement({
8164
- element,
8165
- index,
8166
- canvas: twickCanvas,
8167
- canvasMetadata: canvasMetadataRef.current
8168
- });
8169
- break;
8170
- case ELEMENT_TYPES.CIRCLE:
8171
- await addCircleElement({
8172
- element,
8173
- index,
8174
- canvas: twickCanvas,
8175
- canvasMetadata: canvasMetadataRef.current
8176
- });
8177
- break;
8178
- case ELEMENT_TYPES.TEXT:
8179
- await addTextElement({
8180
- element,
8181
- index,
8182
- canvas: twickCanvas,
8183
- canvasMetadata: canvasMetadataRef.current
8184
- });
8185
- break;
8186
- case ELEMENT_TYPES.CAPTION:
8187
- await addCaptionElement({
8188
- element,
8189
- index,
8190
- canvas: twickCanvas,
8191
- captionProps,
8192
- canvasMetadata: canvasMetadataRef.current
8193
- });
8194
- break;
8700
+ const handler = elementController.get(element.type);
8701
+ if (handler) {
8702
+ await handler.add({
8703
+ element,
8704
+ index,
8705
+ canvas: twickCanvas,
8706
+ canvasMetadata: canvasMetadataRef.current,
8707
+ seekTime,
8708
+ captionProps: captionProps ?? null,
8709
+ elementFrameMapRef: elementFrameMap,
8710
+ getCurrentFrameEffect,
8711
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
8712
+ });
8195
8713
  }
8196
- elementMap.current[element.id] = element;
8714
+ elementMap.current[element.id] = { ...element, zIndex: element.zIndex ?? index };
8197
8715
  if (reorder) {
8198
8716
  reorderElementsByZIndex(twickCanvas);
8199
8717
  }
8200
8718
  };
8201
- return {
8202
- twickCanvas,
8203
- buildCanvas,
8204
- onVideoSizeChange,
8205
- addElementToCanvas,
8206
- setCanvasElements
8207
- };
8208
- };
8209
- const usePlayerManager = ({
8210
- videoProps
8211
- }) => {
8212
- const [projectData, setProjectData] = React.useState(null);
8213
- const {
8214
- timelineAction,
8215
- setTimelineAction,
8216
- setSelectedItem,
8217
- editor,
8218
- changeLog
8219
- } = timeline.useTimelineContext();
8220
- const currentChangeLog = React.useRef(changeLog);
8221
- const prevSeekTime = React.useRef(0);
8222
- const [playerUpdating, setPlayerUpdating] = React.useState(false);
8223
- const handleCanvasReady = (_canvas) => {
8224
- };
8225
- const handleCanvasOperation = (operation, data) => {
8226
- if (operation === CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED) {
8227
- const subtitlesTrack = editor.getSubtiltesTrack();
8228
- subtitlesTrack == null ? void 0 : subtitlesTrack.setProps(data.props);
8229
- setSelectedItem(data.element);
8230
- editor.refresh();
8231
- } else {
8232
- const element = timeline.ElementDeserializer.fromJSON(data);
8233
- switch (operation) {
8234
- case CANVAS_OPERATIONS.ITEM_SELECTED:
8235
- setSelectedItem(element);
8236
- break;
8237
- case CANVAS_OPERATIONS.ITEM_UPDATED:
8238
- if (element) {
8239
- const updatedElement = editor.updateElement(element);
8240
- currentChangeLog.current = currentChangeLog.current + 1;
8241
- setSelectedItem(updatedElement);
8242
- }
8243
- break;
8244
- }
8245
- }
8246
- };
8247
- const { twickCanvas, buildCanvas, setCanvasElements } = useTwickCanvas({
8248
- onCanvasReady: handleCanvasReady,
8249
- onCanvasOperation: handleCanvasOperation
8250
- });
8251
- const updateCanvas = (seekTime) => {
8252
- var _a;
8253
- if (changeLog === currentChangeLog.current && seekTime === prevSeekTime.current) {
8254
- return;
8719
+ const addWatermarkToCanvas = ({
8720
+ element
8721
+ }) => {
8722
+ if (!twickCanvas) return;
8723
+ const handler = elementController.get("watermark");
8724
+ if (handler) {
8725
+ handler.add({
8726
+ element,
8727
+ index: Object.keys(elementMap.current).length,
8728
+ canvas: twickCanvas,
8729
+ canvasMetadata: canvasMetadataRef.current,
8730
+ watermarkPropsRef
8731
+ });
8732
+ elementMap.current[element.id] = element;
8255
8733
  }
8256
- prevSeekTime.current = seekTime;
8257
- const elements = timeline.getCurrentElements(
8258
- seekTime,
8259
- ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? []
8260
- );
8261
- let captionProps = {};
8262
- (elements || []).forEach((element) => {
8263
- if (element instanceof timeline.CaptionElement) {
8264
- const track = editor.getTrackById(element.getTrackId());
8265
- captionProps = (track == null ? void 0 : track.getProps()) ?? {};
8266
- }
8267
- });
8268
- setCanvasElements({
8269
- elements,
8270
- seekTime,
8271
- captionProps,
8272
- cleanAndAdd: true
8273
- });
8274
- currentChangeLog.current = changeLog;
8275
8734
  };
8276
- const onPlayerUpdate = (event) => {
8277
- var _a;
8278
- if (((_a = event == null ? void 0 : event.detail) == null ? void 0 : _a.status) === "ready") {
8279
- setPlayerUpdating(false);
8280
- setTimelineAction(timeline.TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
8281
- }
8735
+ const applyZOrder = (elementId, direction) => {
8736
+ if (!twickCanvas) return false;
8737
+ const newZIndex = changeZOrder(twickCanvas, elementId, direction);
8738
+ if (newZIndex == null) return false;
8739
+ const element = elementMap.current[elementId];
8740
+ if (element) elementMap.current[elementId] = { ...element, zIndex: newZIndex };
8741
+ onCanvasOperation == null ? void 0 : onCanvasOperation(CANVAS_OPERATIONS.Z_ORDER_CHANGED, { elementId, direction });
8742
+ return true;
8282
8743
  };
8283
- React.useEffect(() => {
8284
- var _a, _b, _c, _d, _e2;
8285
- switch (timelineAction.type) {
8286
- case timeline.TIMELINE_ACTION.UPDATE_PLAYER_DATA:
8287
- if (videoProps) {
8288
- if (((_a = timelineAction.payload) == null ? void 0 : _a.forceUpdate) || editor.getLatestVersion() !== ((_b = projectData == null ? void 0 : projectData.input) == null ? void 0 : _b.version)) {
8289
- setPlayerUpdating(true);
8290
- const _latestProjectData = {
8291
- input: {
8292
- properties: videoProps,
8293
- tracks: ((_c = timelineAction.payload) == null ? void 0 : _c.tracks) ?? [],
8294
- version: ((_d = timelineAction.payload) == null ? void 0 : _d.version) ?? 0
8295
- }
8296
- };
8297
- setProjectData(_latestProjectData);
8298
- if (((_e2 = timelineAction.payload) == null ? void 0 : _e2.version) === 1) {
8299
- setTimeout(() => {
8300
- setPlayerUpdating(false);
8301
- });
8302
- }
8303
- } else {
8304
- setTimelineAction(timeline.TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
8305
- }
8306
- }
8307
- break;
8308
- }
8309
- }, [timelineAction]);
8744
+ const bringToFront = (elementId) => applyZOrder(elementId, "front");
8745
+ const sendToBack = (elementId) => applyZOrder(elementId, "back");
8746
+ const bringForward = (elementId) => applyZOrder(elementId, "forward");
8747
+ const sendBackward = (elementId) => applyZOrder(elementId, "backward");
8310
8748
  return {
8311
8749
  twickCanvas,
8312
- projectData,
8313
- updateCanvas,
8314
8750
  buildCanvas,
8315
- onPlayerUpdate,
8316
- playerUpdating
8751
+ onVideoSizeChange,
8752
+ addWatermarkToCanvas,
8753
+ addElementToCanvas,
8754
+ setCanvasElements,
8755
+ bringToFront,
8756
+ sendToBack,
8757
+ bringForward,
8758
+ sendBackward
8317
8759
  };
8318
8760
  };
8319
- const PlayerManager = ({
8320
- videoProps,
8321
- playerProps,
8322
- canvasMode
8323
- }) => {
8324
- const { changeLog } = timeline.useTimelineContext();
8325
- const { twickCanvas, projectData, updateCanvas, playerUpdating, onPlayerUpdate, buildCanvas } = usePlayerManager({ videoProps });
8326
- const durationRef = React.useRef(0);
8327
- const {
8328
- playerState,
8329
- playerVolume,
8330
- seekTime,
8331
- setPlayerState,
8332
- setCurrentTime
8333
- } = livePlayer.useLivePlayerContext();
8334
- const containerRef = React.useRef(null);
8335
- const canvasRef = React.useRef(null);
8336
- React.useEffect(() => {
8337
- const container = containerRef.current;
8338
- const canvasSize = {
8339
- width: container == null ? void 0 : container.clientWidth,
8340
- height: container == null ? void 0 : container.clientHeight
8341
- };
8342
- buildCanvas({
8343
- backgroundColor: videoProps.backgroundColor,
8344
- videoSize: {
8345
- width: videoProps.width,
8346
- height: videoProps.height
8347
- },
8348
- canvasSize,
8349
- canvasRef: canvasRef.current
8350
- });
8351
- }, [videoProps]);
8352
- React.useEffect(() => {
8353
- if (twickCanvas && playerState === livePlayer.PLAYER_STATE.PAUSED) {
8354
- updateCanvas(seekTime);
8355
- }
8356
- }, [twickCanvas, playerState, seekTime, changeLog]);
8357
- const handleTimeUpdate = (time2) => {
8358
- if (durationRef.current && time2 >= durationRef.current) {
8359
- setCurrentTime(0);
8360
- setPlayerState(livePlayer.PLAYER_STATE.PAUSED);
8361
- } else {
8362
- setCurrentTime(time2);
8363
- }
8364
- };
8365
- return /* @__PURE__ */ jsxRuntime.jsxs(
8366
- "div",
8761
+ const VIDEO_TYPES = [
8762
+ "video/mp4",
8763
+ "video/webm",
8764
+ "video/ogg",
8765
+ "video/quicktime",
8766
+ "video/x-msvideo",
8767
+ "video/x-matroska"
8768
+ ];
8769
+ const AUDIO_TYPES = [
8770
+ "audio/mpeg",
8771
+ "audio/mp3",
8772
+ "audio/wav",
8773
+ "audio/ogg",
8774
+ "audio/webm",
8775
+ "audio/aac",
8776
+ "audio/mp4",
8777
+ "audio/x-wav"
8778
+ ];
8779
+ const IMAGE_TYPES = [
8780
+ "image/jpeg",
8781
+ "image/jpg",
8782
+ "image/png",
8783
+ "image/gif",
8784
+ "image/webp",
8785
+ "image/svg+xml",
8786
+ "image/bmp"
8787
+ ];
8788
+ const EXT_TO_TYPE = {
8789
+ mp4: "video",
8790
+ webm: "video",
8791
+ mov: "video",
8792
+ avi: "video",
8793
+ mkv: "video",
8794
+ mp3: "audio",
8795
+ wav: "audio",
8796
+ ogg: "audio",
8797
+ m4a: "audio",
8798
+ jpg: "image",
8799
+ jpeg: "image",
8800
+ png: "image",
8801
+ gif: "image",
8802
+ webp: "image",
8803
+ svg: "image",
8804
+ bmp: "image"
8805
+ };
8806
+ function getAssetTypeFromFile(file) {
8807
+ var _a;
8808
+ const mime = (file.type || "").toLowerCase();
8809
+ const ext = (((_a = file.name) == null ? void 0 : _a.split(".").pop()) || "").toLowerCase();
8810
+ if (VIDEO_TYPES.some((t2) => mime.includes(t2))) return "video";
8811
+ if (AUDIO_TYPES.some((t2) => mime.includes(t2))) return "audio";
8812
+ if (IMAGE_TYPES.some((t2) => mime.includes(t2))) return "image";
8813
+ if (ext && EXT_TO_TYPE[ext]) return EXT_TO_TYPE[ext];
8814
+ return null;
8815
+ }
8816
+ const INITIAL_TIMELINE_DATA = {
8817
+ tracks: [
8367
8818
  {
8368
- className: "twick-editor-container",
8369
- style: {
8370
- aspectRatio: `${videoProps.width}/${videoProps.height}`
8371
- },
8372
- children: [
8373
- /* @__PURE__ */ jsxRuntime.jsx(
8374
- "div",
8375
- {
8376
- className: "twick-editor-loading-overlay",
8377
- style: {
8378
- display: playerUpdating ? "flex" : "none"
8379
- },
8380
- children: playerUpdating ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-editor-loading-spinner" }) : null
8381
- }
8382
- ),
8383
- /* @__PURE__ */ jsxRuntime.jsx(
8384
- livePlayer.LivePlayer,
8385
- {
8386
- seekTime,
8387
- projectData,
8388
- quality: (playerProps == null ? void 0 : playerProps.quality) || 1,
8389
- videoSize: {
8390
- width: videoProps.width,
8391
- height: videoProps.height
8392
- },
8393
- onPlayerUpdate,
8394
- containerStyle: {
8395
- opacity: canvasMode ? playerState === livePlayer.PLAYER_STATE.PAUSED ? 0 : 1 : 1
8396
- },
8397
- onTimeUpdate: handleTimeUpdate,
8398
- volume: playerVolume,
8399
- onDurationChange: (duration) => {
8400
- durationRef.current = duration;
8401
- },
8402
- playing: playerState === livePlayer.PLAYER_STATE.PLAYING
8403
- }
8404
- ),
8405
- canvasMode && /* @__PURE__ */ jsxRuntime.jsx(
8406
- "div",
8407
- {
8408
- ref: containerRef,
8409
- className: "twick-editor-canvas-container",
8410
- style: {
8411
- opacity: playerState === livePlayer.PLAYER_STATE.PAUSED ? 1 : 0
8412
- },
8413
- children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "twick-editor-canvas" })
8819
+ type: "element",
8820
+ id: "t-sample",
8821
+ name: "sample",
8822
+ elements: [
8823
+ {
8824
+ id: "e-sample",
8825
+ trackId: "t-sample",
8826
+ name: "sample",
8827
+ type: "text",
8828
+ s: 0,
8829
+ e: 5,
8830
+ props: {
8831
+ text: "Twick Video Editor",
8832
+ fill: "#FFFFFF"
8414
8833
  }
8415
- )
8834
+ }
8416
8835
  ]
8417
8836
  }
8418
- );
8837
+ ],
8838
+ version: 1
8419
8839
  };
8420
- function clamp$1(v2, min, max) {
8421
- return Math.max(min, Math.min(v2, max));
8422
- }
8423
- const V = {
8424
- toVector(v2, fallback) {
8425
- if (v2 === void 0) v2 = fallback;
8426
- return Array.isArray(v2) ? v2 : [v2, v2];
8840
+ const MIN_DURATION = 0.1;
8841
+ const TIMELINE_DROP_MEDIA_TYPE = "application/x-twick-media";
8842
+ const DRAG_TYPE = {
8843
+ /** Drag operation is starting */
8844
+ START: "start",
8845
+ /** Drag operation is in progress */
8846
+ MOVE: "move",
8847
+ /** Drag operation has ended */
8848
+ END: "end"
8849
+ };
8850
+ const DEFAULT_TIMELINE_ZOOM = 1.5;
8851
+ const DEFAULT_FPS = 30;
8852
+ const SNAP_THRESHOLD_PX = 10;
8853
+ const DEFAULT_TIMELINE_ZOOM_CONFIG = {
8854
+ /** Minimum zoom level (10%) */
8855
+ min: 0.1,
8856
+ /** Maximum zoom level (300%) */
8857
+ max: 3,
8858
+ /** Zoom step increment/decrement (10%) */
8859
+ step: 0.1,
8860
+ /** Default zoom level (150%) */
8861
+ default: 1.5
8862
+ };
8863
+ const DEFAULT_TIMELINE_TICK_CONFIGS = [
8864
+ {
8865
+ durationThreshold: 10,
8866
+ // < 10 seconds
8867
+ majorInterval: 1,
8868
+ // 1s major ticks
8869
+ minorTicks: 10
8870
+ // 0.1s minor ticks (10 minors between majors)
8871
+ },
8872
+ {
8873
+ durationThreshold: 30,
8874
+ // < 30 seconds
8875
+ majorInterval: 5,
8876
+ // 5s major ticks
8877
+ minorTicks: 5
8878
+ // 1s minor ticks (5 minors between majors)
8879
+ },
8880
+ {
8881
+ durationThreshold: 120,
8882
+ // < 2 minutes
8883
+ majorInterval: 10,
8884
+ // 10s major ticks
8885
+ minorTicks: 5
8886
+ // 2s minor ticks (5 minors between majors)
8887
+ },
8888
+ {
8889
+ durationThreshold: 300,
8890
+ // < 5 minutes
8891
+ majorInterval: 30,
8892
+ // 30s major ticks
8893
+ minorTicks: 6
8894
+ // 5s minor ticks (6 minors between majors)
8895
+ },
8896
+ {
8897
+ durationThreshold: 900,
8898
+ // < 15 minutes
8899
+ majorInterval: 60,
8900
+ // 1m major ticks
8901
+ minorTicks: 6
8902
+ // 10s minor ticks (6 minors between majors)
8903
+ },
8904
+ {
8905
+ durationThreshold: 1800,
8906
+ // < 30 minutes
8907
+ majorInterval: 120,
8908
+ // 2m major ticks
8909
+ minorTicks: 4
8910
+ // 30s minor ticks (4 minors between majors)
8911
+ },
8912
+ {
8913
+ durationThreshold: 3600,
8914
+ // < 1 hour
8915
+ majorInterval: 300,
8916
+ // 5m major ticks
8917
+ minorTicks: 5
8918
+ // 1m minor ticks (5 minors between majors)
8919
+ },
8920
+ {
8921
+ durationThreshold: 7200,
8922
+ // < 2 hours
8923
+ majorInterval: 600,
8924
+ // 10m major ticks
8925
+ minorTicks: 10
8926
+ // 1m minor ticks (10 minors between majors)
8927
+ },
8928
+ {
8929
+ durationThreshold: Infinity,
8930
+ // >= 2 hours
8931
+ majorInterval: 1800,
8932
+ // 30m major ticks
8933
+ minorTicks: 6
8934
+ // 5m minor ticks (6 minors between majors)
8935
+ }
8936
+ ];
8937
+ const DEFAULT_ELEMENT_COLORS = {
8938
+ /** Fragment element color - deep charcoal matching UI background */
8939
+ fragment: "#1A1A1A",
8940
+ /** Video element color - vibrant royal purple */
8941
+ video: "#8B5FBF",
8942
+ /** Caption element color - soft wisteria purple */
8943
+ caption: "#9B8ACE",
8944
+ /** Image element color - warm copper accent */
8945
+ image: "#D4956C",
8946
+ /** Audio element color - deep teal */
8947
+ audio: "#3D8B8B",
8948
+ /** Text element color - medium lavender */
8949
+ text: "#8D74C4",
8950
+ /** Generic element color - muted amethyst */
8951
+ element: "#7B68B8",
8952
+ /** Rectangle element color - deep indigo */
8953
+ rect: "#5B4B99",
8954
+ /** Frame effect color - rich magenta */
8955
+ frameEffect: "#B55B9C",
8956
+ /** Filters color - periwinkle blue */
8957
+ filters: "#7A89D4",
8958
+ /** Transition color - burnished bronze */
8959
+ transition: "#BE8157",
8960
+ /** Animation color - muted emerald */
8961
+ animation: "#4B9B78",
8962
+ /** Icon element color - bright orchid */
8963
+ icon: "#A76CD4",
8964
+ /** Circle element color - deep byzantium */
8965
+ circle: "#703D8B"
8966
+ };
8967
+ const AVAILABLE_TEXT_FONTS = {
8968
+ // Google Fonts
8969
+ /** Modern sans-serif font */
8970
+ RUBIK: "Rubik",
8971
+ /** Clean and readable font */
8972
+ MULISH: "Mulish",
8973
+ /** Bold display font */
8974
+ LUCKIEST_GUY: "Luckiest Guy",
8975
+ /** Elegant serif font */
8976
+ PLAYFAIR_DISPLAY: "Playfair Display",
8977
+ /** Classic sans-serif font */
8978
+ ROBOTO: "Roboto",
8979
+ /** Modern geometric font */
8980
+ POPPINS: "Poppins",
8981
+ // Display and Decorative Fonts
8982
+ /** Comic-style display font */
8983
+ BANGERS: "Bangers",
8984
+ /** Handwritten-style font */
8985
+ BIRTHSTONE: "Birthstone",
8986
+ /** Elegant script font */
8987
+ CORINTHIA: "Corinthia",
8988
+ /** Formal script font */
8989
+ IMPERIAL_SCRIPT: "Imperial Script",
8990
+ /** Bold outline font */
8991
+ KUMAR_ONE_OUTLINE: "Kumar One Outline",
8992
+ /** Light outline font */
8993
+ LONDRI_OUTLINE: "Londrina Outline",
8994
+ /** Casual script font */
8995
+ MARCK_SCRIPT: "Marck Script",
8996
+ /** Modern sans-serif font */
8997
+ MONTSERRAT: "Montserrat",
8998
+ /** Stylish display font */
8999
+ PATTAYA: "Pattaya",
9000
+ // CDN Fonts
9001
+ /** Unique display font */
9002
+ PERALTA: "Peralta",
9003
+ /** Bold impact font */
9004
+ IMPACT: "Impact",
9005
+ /** Handwritten-style font */
9006
+ LUMANOSIMO: "Lumanosimo",
9007
+ /** Custom display font */
9008
+ KAPAKANA: "Kapakana",
9009
+ /** Handwritten font */
9010
+ HANDYRUSH: "HandyRush",
9011
+ /** Decorative font */
9012
+ DASHER: "Dasher",
9013
+ /** Signature-style font */
9014
+ BRITTANY_SIGNATURE: "Brittany Signature"
9015
+ };
9016
+ const DEFAULT_DROP_DURATION = 5;
9017
+ function useTimelineDrop({
9018
+ containerRef,
9019
+ scrollContainerRef,
9020
+ tracks,
9021
+ duration,
9022
+ zoomLevel,
9023
+ labelWidth,
9024
+ trackHeight,
9025
+ /** Width of the track content area (timeline minus labels). Used for accurate time mapping. */
9026
+ trackContentWidth,
9027
+ onDrop,
9028
+ enabled = true
9029
+ }) {
9030
+ const [preview, setPreview] = React.useState(null);
9031
+ const [isDraggingOver, setIsDraggingOver] = React.useState(false);
9032
+ const computePosition = React.useCallback(
9033
+ (clientX, clientY) => {
9034
+ var _a, _b;
9035
+ if (!containerRef.current) return null;
9036
+ const rect = containerRef.current.getBoundingClientRect();
9037
+ const scrollEl = (scrollContainerRef == null ? void 0 : scrollContainerRef.current) ?? containerRef.current;
9038
+ const scrollLeft = (scrollEl == null ? void 0 : scrollEl.scrollLeft) ?? 0;
9039
+ const viewportLeft = ((_b = (_a = scrollEl == null ? void 0 : scrollEl.getBoundingClientRect) == null ? void 0 : _a.call(scrollEl)) == null ? void 0 : _b.left) ?? rect.left;
9040
+ const contentX = clientX - viewportLeft + scrollLeft - labelWidth;
9041
+ const relY = clientY - rect.top;
9042
+ const rawTrackIndex = Math.floor(relY / trackHeight);
9043
+ const trackIndex = tracks.length === 0 ? 0 : Math.max(0, Math.min(tracks.length - 1, rawTrackIndex));
9044
+ const pixelsPerSecond = trackContentWidth != null && trackContentWidth > 0 ? trackContentWidth / duration : 100 * zoomLevel;
9045
+ const timeSec = Math.max(
9046
+ 0,
9047
+ Math.min(duration, contentX / pixelsPerSecond)
9048
+ );
9049
+ return { trackIndex, timeSec };
9050
+ },
9051
+ [
9052
+ containerRef,
9053
+ scrollContainerRef,
9054
+ tracks.length,
9055
+ labelWidth,
9056
+ trackHeight,
9057
+ zoomLevel,
9058
+ duration,
9059
+ trackContentWidth
9060
+ ]
9061
+ );
9062
+ const handleDragOver = React.useCallback(
9063
+ (e3) => {
9064
+ if (!enabled) return;
9065
+ e3.preventDefault();
9066
+ e3.stopPropagation();
9067
+ const hasFiles = e3.dataTransfer.types.includes("Files");
9068
+ const hasPanelMedia = e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE);
9069
+ if (!hasFiles && !hasPanelMedia) return;
9070
+ e3.dataTransfer.dropEffect = "copy";
9071
+ setIsDraggingOver(true);
9072
+ const pos = computePosition(e3.clientX, e3.clientY);
9073
+ if (pos) {
9074
+ const track = tracks[pos.trackIndex] ?? null;
9075
+ if (track || tracks.length === 0) {
9076
+ setPreview({
9077
+ trackIndex: pos.trackIndex,
9078
+ timeSec: pos.timeSec,
9079
+ widthPct: Math.min(
9080
+ 100,
9081
+ DEFAULT_DROP_DURATION / duration * 100
9082
+ )
9083
+ });
9084
+ }
9085
+ }
9086
+ },
9087
+ [enabled, computePosition, tracks, duration]
9088
+ );
9089
+ const handleDragLeave = React.useCallback((e3) => {
9090
+ if (!e3.currentTarget.contains(e3.relatedTarget)) {
9091
+ setIsDraggingOver(false);
9092
+ setPreview(null);
9093
+ }
9094
+ }, []);
9095
+ const handleDrop = React.useCallback(
9096
+ async (e3) => {
9097
+ if (!enabled) return;
9098
+ e3.preventDefault();
9099
+ e3.stopPropagation();
9100
+ setIsDraggingOver(false);
9101
+ setPreview(null);
9102
+ const pos = computePosition(e3.clientX, e3.clientY);
9103
+ if (!pos || pos.trackIndex < 0) return;
9104
+ const track = tracks[pos.trackIndex] ?? null;
9105
+ if (e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE)) {
9106
+ try {
9107
+ const data = JSON.parse(
9108
+ e3.dataTransfer.getData(TIMELINE_DROP_MEDIA_TYPE) || "{}"
9109
+ );
9110
+ if (data.type && data.url) {
9111
+ await onDrop({
9112
+ track,
9113
+ timeSec: pos.timeSec,
9114
+ type: data.type,
9115
+ url: data.url
9116
+ });
9117
+ }
9118
+ } catch {
9119
+ }
9120
+ return;
9121
+ }
9122
+ const files = Array.from(e3.dataTransfer.files || []);
9123
+ for (const file of files) {
9124
+ const type = getAssetTypeFromFile(file);
9125
+ if (!type) continue;
9126
+ const blobUrl = URL.createObjectURL(file);
9127
+ try {
9128
+ await onDrop({
9129
+ track,
9130
+ timeSec: pos.timeSec,
9131
+ type,
9132
+ url: blobUrl
9133
+ });
9134
+ } finally {
9135
+ URL.revokeObjectURL(blobUrl);
9136
+ }
9137
+ break;
9138
+ }
9139
+ },
9140
+ [enabled, computePosition, tracks, onDrop]
9141
+ );
9142
+ return { preview, isDraggingOver, handleDragOver, handleDragLeave, handleDrop };
9143
+ }
9144
+ function createElementFromDrop(type, blobUrl, parentSize) {
9145
+ switch (type) {
9146
+ case "video":
9147
+ return new timeline.VideoElement(blobUrl, parentSize);
9148
+ case "audio":
9149
+ return new timeline.AudioElement(blobUrl);
9150
+ case "image":
9151
+ return new timeline.ImageElement(blobUrl, parentSize);
9152
+ default:
9153
+ throw new Error(`Unknown asset type: ${type}`);
9154
+ }
9155
+ }
9156
+ const usePlayerManager = ({
9157
+ videoProps,
9158
+ canvasConfig
9159
+ }) => {
9160
+ const [projectData, setProjectData] = React.useState(null);
9161
+ const {
9162
+ timelineAction,
9163
+ setTimelineAction,
9164
+ setSelectedItem,
9165
+ editor,
9166
+ changeLog,
9167
+ videoResolution
9168
+ } = timeline.useTimelineContext();
9169
+ const { getCurrentTime } = livePlayer.useLivePlayerContext();
9170
+ const currentChangeLog = React.useRef(changeLog);
9171
+ const prevSeekTime = React.useRef(0);
9172
+ const [playerUpdating, setPlayerUpdating] = React.useState(false);
9173
+ const handleCanvasReady = (_canvas) => {
9174
+ };
9175
+ const handleCanvasOperation = (operation, data) => {
9176
+ var _a;
9177
+ if (operation === CANVAS_OPERATIONS.ADDED_NEW_ELEMENT) {
9178
+ if (data == null ? void 0 : data.element) {
9179
+ setSelectedItem(data.element);
9180
+ }
9181
+ return;
9182
+ }
9183
+ if (operation === CANVAS_OPERATIONS.Z_ORDER_CHANGED) {
9184
+ const { elementId, direction } = data ?? {};
9185
+ if (!elementId || !direction) return;
9186
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
9187
+ const trackIndex = tracks.findIndex(
9188
+ (t2) => t2.getElements().some((el) => el.getId() === elementId)
9189
+ );
9190
+ if (trackIndex < 0) return;
9191
+ const track = tracks[trackIndex];
9192
+ const reordered = [...tracks];
9193
+ if (direction === "front") {
9194
+ reordered.splice(trackIndex, 1);
9195
+ reordered.push(track);
9196
+ } else if (direction === "back") {
9197
+ reordered.splice(trackIndex, 1);
9198
+ reordered.unshift(track);
9199
+ } else if (direction === "forward" && trackIndex < reordered.length - 1) {
9200
+ [reordered[trackIndex], reordered[trackIndex + 1]] = [reordered[trackIndex + 1], reordered[trackIndex]];
9201
+ } else if (direction === "backward" && trackIndex > 0) {
9202
+ [reordered[trackIndex - 1], reordered[trackIndex]] = [reordered[trackIndex], reordered[trackIndex - 1]];
9203
+ } else {
9204
+ return;
9205
+ }
9206
+ editor.reorderTracks(reordered);
9207
+ currentChangeLog.current = currentChangeLog.current + 1;
9208
+ return;
9209
+ }
9210
+ if (operation === CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED) {
9211
+ const captionsTrack = editor.getCaptionsTrack();
9212
+ captionsTrack == null ? void 0 : captionsTrack.setProps(data.props);
9213
+ setSelectedItem(data.element);
9214
+ editor.refresh();
9215
+ } else if (operation === CANVAS_OPERATIONS.WATERMARK_UPDATED) {
9216
+ const w2 = editor.getWatermark();
9217
+ if (w2 && data) {
9218
+ if (data.position) w2.setPosition(data.position);
9219
+ if (data.rotation != null) w2.setRotation(data.rotation);
9220
+ if (data.opacity != null) w2.setOpacity(data.opacity);
9221
+ if (data.props) w2.setProps(data.props);
9222
+ editor.setWatermark(w2);
9223
+ currentChangeLog.current = currentChangeLog.current + 1;
9224
+ }
9225
+ } else {
9226
+ const element = timeline.ElementDeserializer.fromJSON(data);
9227
+ switch (operation) {
9228
+ case CANVAS_OPERATIONS.ITEM_SELECTED:
9229
+ setSelectedItem(element);
9230
+ break;
9231
+ case CANVAS_OPERATIONS.ITEM_UPDATED:
9232
+ if (element) {
9233
+ const updatedElement = editor.updateElement(element);
9234
+ currentChangeLog.current = currentChangeLog.current + 1;
9235
+ setSelectedItem(updatedElement);
9236
+ }
9237
+ break;
9238
+ }
9239
+ }
9240
+ };
9241
+ const handleDropOnCanvas = React.useCallback(
9242
+ async (payload) => {
9243
+ const { type, url, canvasX, canvasY } = payload;
9244
+ const element = createElementFromDrop(type, url, videoResolution);
9245
+ const currentTime = getCurrentTime();
9246
+ element.setStart(currentTime);
9247
+ const newTrack = editor.addTrack(`Track_${Date.now()}`);
9248
+ const result = await editor.addElementToTrack(newTrack, element);
9249
+ if (result) {
9250
+ setSelectedItem(element);
9251
+ currentChangeLog.current = currentChangeLog.current + 1;
9252
+ editor.refresh();
9253
+ handleCanvasOperation(CANVAS_OPERATIONS.ADDED_NEW_ELEMENT, {
9254
+ element,
9255
+ canvasPosition: canvasX != null && canvasY != null ? { x: canvasX, y: canvasY } : void 0
9256
+ });
9257
+ }
9258
+ },
9259
+ [editor, videoResolution, getCurrentTime, setSelectedItem]
9260
+ );
9261
+ const {
9262
+ twickCanvas,
9263
+ buildCanvas,
9264
+ setCanvasElements,
9265
+ bringToFront,
9266
+ sendToBack,
9267
+ bringForward,
9268
+ sendBackward
9269
+ } = useTwickCanvas({
9270
+ onCanvasReady: handleCanvasReady,
9271
+ onCanvasOperation: handleCanvasOperation,
9272
+ enableShiftAxisLock: (canvasConfig == null ? void 0 : canvasConfig.enableShiftAxisLock) ?? false
9273
+ });
9274
+ const updateCanvas = (seekTime) => {
9275
+ var _a;
9276
+ if (changeLog === currentChangeLog.current && seekTime === prevSeekTime.current) {
9277
+ return;
9278
+ }
9279
+ prevSeekTime.current = seekTime;
9280
+ const elements = timeline.getCurrentElements(
9281
+ seekTime,
9282
+ ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? []
9283
+ );
9284
+ let captionProps = {};
9285
+ (elements || []).forEach((element) => {
9286
+ if (element instanceof timeline.CaptionElement) {
9287
+ const track = editor.getTrackById(element.getTrackId());
9288
+ captionProps = (track == null ? void 0 : track.getProps()) ?? {};
9289
+ }
9290
+ });
9291
+ const watermark = editor.getWatermark();
9292
+ let watermarkElement;
9293
+ if (watermark) {
9294
+ const position = watermark.getPosition();
9295
+ watermarkElement = {
9296
+ id: watermark.getId(),
9297
+ type: watermark.getType(),
9298
+ props: {
9299
+ ...watermark.getProps() || {},
9300
+ x: (position == null ? void 0 : position.x) ?? 0,
9301
+ y: (position == null ? void 0 : position.y) ?? 0,
9302
+ rotation: watermark.getRotation() ?? 0,
9303
+ opacity: watermark.getOpacity() ?? 1
9304
+ }
9305
+ };
9306
+ }
9307
+ setCanvasElements({
9308
+ elements,
9309
+ watermark: watermarkElement,
9310
+ seekTime,
9311
+ captionProps,
9312
+ cleanAndAdd: true,
9313
+ lockAspectRatio: canvasConfig == null ? void 0 : canvasConfig.lockAspectRatio
9314
+ });
9315
+ currentChangeLog.current = changeLog;
9316
+ };
9317
+ const onPlayerUpdate = (event) => {
9318
+ var _a;
9319
+ if (((_a = event == null ? void 0 : event.detail) == null ? void 0 : _a.status) === "ready") {
9320
+ setPlayerUpdating(false);
9321
+ setTimelineAction(timeline.TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
9322
+ }
9323
+ };
9324
+ const deleteElement = (elementId) => {
9325
+ var _a;
9326
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
9327
+ for (const track of tracks) {
9328
+ const element = track.getElementById(elementId);
9329
+ if (element) {
9330
+ editor.removeElement(element);
9331
+ currentChangeLog.current = currentChangeLog.current + 1;
9332
+ setSelectedItem(null);
9333
+ updateCanvas(getCurrentTime());
9334
+ return;
9335
+ }
9336
+ }
9337
+ };
9338
+ React.useEffect(() => {
9339
+ var _a, _b, _c, _d, _e2;
9340
+ switch (timelineAction.type) {
9341
+ case timeline.TIMELINE_ACTION.UPDATE_PLAYER_DATA:
9342
+ if (videoProps) {
9343
+ if (((_a = timelineAction.payload) == null ? void 0 : _a.forceUpdate) || editor.getLatestVersion() !== ((_b = projectData == null ? void 0 : projectData.input) == null ? void 0 : _b.version)) {
9344
+ setPlayerUpdating(true);
9345
+ const _latestProjectData = {
9346
+ input: {
9347
+ properties: videoProps,
9348
+ tracks: ((_c = timelineAction.payload) == null ? void 0 : _c.tracks) ?? [],
9349
+ version: ((_d = timelineAction.payload) == null ? void 0 : _d.version) ?? 0
9350
+ }
9351
+ };
9352
+ setProjectData(_latestProjectData);
9353
+ if (((_e2 = timelineAction.payload) == null ? void 0 : _e2.version) === 1) {
9354
+ setTimeout(() => {
9355
+ setPlayerUpdating(false);
9356
+ });
9357
+ }
9358
+ } else {
9359
+ setTimelineAction(timeline.TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
9360
+ }
9361
+ }
9362
+ break;
9363
+ }
9364
+ }, [timelineAction]);
9365
+ return {
9366
+ twickCanvas,
9367
+ projectData,
9368
+ updateCanvas,
9369
+ buildCanvas,
9370
+ onPlayerUpdate,
9371
+ playerUpdating,
9372
+ handleDropOnCanvas,
9373
+ bringToFront,
9374
+ sendToBack,
9375
+ bringForward,
9376
+ sendBackward,
9377
+ deleteElement
9378
+ };
9379
+ };
9380
+ function useCanvasDrop({
9381
+ containerRef,
9382
+ videoSize,
9383
+ onDrop,
9384
+ enabled = true
9385
+ }) {
9386
+ const handleDragOver = React.useCallback(
9387
+ (e3) => {
9388
+ if (!enabled) return;
9389
+ e3.preventDefault();
9390
+ e3.stopPropagation();
9391
+ const hasFiles = e3.dataTransfer.types.includes("Files");
9392
+ const hasPanelMedia = e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE);
9393
+ if (!hasFiles && !hasPanelMedia) return;
9394
+ e3.dataTransfer.dropEffect = "copy";
9395
+ },
9396
+ [enabled]
9397
+ );
9398
+ const handleDrop = React.useCallback(
9399
+ async (e3) => {
9400
+ if (!enabled || !containerRef.current) return;
9401
+ e3.preventDefault();
9402
+ e3.stopPropagation();
9403
+ let type = null;
9404
+ let url = null;
9405
+ if (e3.dataTransfer.types.includes(TIMELINE_DROP_MEDIA_TYPE)) {
9406
+ try {
9407
+ const data = JSON.parse(
9408
+ e3.dataTransfer.getData(TIMELINE_DROP_MEDIA_TYPE) || "{}"
9409
+ );
9410
+ if (data.type && data.url) {
9411
+ type = data.type;
9412
+ url = data.url;
9413
+ }
9414
+ } catch {
9415
+ }
9416
+ }
9417
+ if (!type || !url) {
9418
+ const files = Array.from(e3.dataTransfer.files || []);
9419
+ for (const file of files) {
9420
+ const detectedType = getAssetTypeFromFile(file);
9421
+ if (detectedType) {
9422
+ type = detectedType;
9423
+ url = URL.createObjectURL(file);
9424
+ try {
9425
+ await onDrop({
9426
+ type,
9427
+ url,
9428
+ canvasX: getCanvasX(e3, containerRef.current, videoSize),
9429
+ canvasY: getCanvasY(e3, containerRef.current, videoSize)
9430
+ });
9431
+ } finally {
9432
+ URL.revokeObjectURL(url);
9433
+ }
9434
+ return;
9435
+ }
9436
+ }
9437
+ return;
9438
+ }
9439
+ await onDrop({
9440
+ type,
9441
+ url,
9442
+ canvasX: getCanvasX(e3, containerRef.current, videoSize),
9443
+ canvasY: getCanvasY(e3, containerRef.current, videoSize)
9444
+ });
9445
+ },
9446
+ [enabled, containerRef, videoSize, onDrop]
9447
+ );
9448
+ const handleDragLeave = React.useCallback((e3) => {
9449
+ if (!e3.currentTarget.contains(e3.relatedTarget)) ;
9450
+ }, []);
9451
+ return { handleDragOver, handleDragLeave, handleDrop };
9452
+ }
9453
+ function getCanvasX(e3, container, videoSize) {
9454
+ const rect = container.getBoundingClientRect();
9455
+ const relX = (e3.clientX - rect.left) / rect.width;
9456
+ return Math.max(0, Math.min(videoSize.width, relX * videoSize.width));
9457
+ }
9458
+ function getCanvasY(e3, container, videoSize) {
9459
+ const rect = container.getBoundingClientRect();
9460
+ const relY = (e3.clientY - rect.top) / rect.height;
9461
+ return Math.max(0, Math.min(videoSize.height, relY * videoSize.height));
9462
+ }
9463
+ const CanvasContextMenu = ({
9464
+ x: x2,
9465
+ y: y2,
9466
+ elementId,
9467
+ onBringToFront,
9468
+ onSendToBack,
9469
+ onBringForward,
9470
+ onSendBackward,
9471
+ onDelete,
9472
+ onClose
9473
+ }) => {
9474
+ const menuRef = React.useRef(null);
9475
+ React.useEffect(() => {
9476
+ const handleClickOutside = (e3) => {
9477
+ if (menuRef.current && !menuRef.current.contains(e3.target)) {
9478
+ onClose();
9479
+ }
9480
+ };
9481
+ const handleEscape = (e3) => {
9482
+ if (e3.key === "Escape") onClose();
9483
+ };
9484
+ document.addEventListener("mousedown", handleClickOutside);
9485
+ document.addEventListener("keydown", handleEscape);
9486
+ return () => {
9487
+ document.removeEventListener("mousedown", handleClickOutside);
9488
+ document.removeEventListener("keydown", handleEscape);
9489
+ };
9490
+ }, [onClose]);
9491
+ const handleAction = (fn2) => {
9492
+ fn2(elementId);
9493
+ onClose();
9494
+ };
9495
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9496
+ "div",
9497
+ {
9498
+ ref: menuRef,
9499
+ className: "twick-canvas-context-menu",
9500
+ style: { left: x2, top: y2 },
9501
+ role: "menu",
9502
+ children: [
9503
+ /* @__PURE__ */ jsxRuntime.jsx(
9504
+ "button",
9505
+ {
9506
+ type: "button",
9507
+ className: "twick-canvas-context-menu-item",
9508
+ onClick: () => handleAction(onBringToFront),
9509
+ role: "menuitem",
9510
+ children: "Bring to Front"
9511
+ }
9512
+ ),
9513
+ /* @__PURE__ */ jsxRuntime.jsx(
9514
+ "button",
9515
+ {
9516
+ type: "button",
9517
+ className: "twick-canvas-context-menu-item",
9518
+ onClick: () => handleAction(onBringForward),
9519
+ role: "menuitem",
9520
+ children: "Bring Forward"
9521
+ }
9522
+ ),
9523
+ /* @__PURE__ */ jsxRuntime.jsx(
9524
+ "button",
9525
+ {
9526
+ type: "button",
9527
+ className: "twick-canvas-context-menu-item",
9528
+ onClick: () => handleAction(onSendBackward),
9529
+ role: "menuitem",
9530
+ children: "Send Backward"
9531
+ }
9532
+ ),
9533
+ /* @__PURE__ */ jsxRuntime.jsx(
9534
+ "button",
9535
+ {
9536
+ type: "button",
9537
+ className: "twick-canvas-context-menu-item",
9538
+ onClick: () => handleAction(onSendToBack),
9539
+ role: "menuitem",
9540
+ children: "Send to Back"
9541
+ }
9542
+ ),
9543
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-canvas-context-menu-separator", role: "separator" }),
9544
+ /* @__PURE__ */ jsxRuntime.jsx(
9545
+ "button",
9546
+ {
9547
+ type: "button",
9548
+ className: "twick-canvas-context-menu-item twick-canvas-context-menu-item-danger",
9549
+ onClick: () => handleAction(onDelete),
9550
+ role: "menuitem",
9551
+ children: "Delete"
9552
+ }
9553
+ )
9554
+ ]
9555
+ }
9556
+ );
9557
+ };
9558
+ const PlayerManager = ({
9559
+ videoProps,
9560
+ playerProps,
9561
+ canvasMode,
9562
+ canvasConfig
9563
+ }) => {
9564
+ const containerRef = React.useRef(null);
9565
+ const canvasRef = React.useRef(null);
9566
+ const durationRef = React.useRef(0);
9567
+ const { changeLog } = timeline.useTimelineContext();
9568
+ const {
9569
+ playerState,
9570
+ playerVolume,
9571
+ seekTime,
9572
+ setPlayerState,
9573
+ setCurrentTime
9574
+ } = livePlayer.useLivePlayerContext();
9575
+ const {
9576
+ twickCanvas,
9577
+ projectData,
9578
+ updateCanvas,
9579
+ playerUpdating,
9580
+ onPlayerUpdate,
9581
+ buildCanvas,
9582
+ handleDropOnCanvas,
9583
+ bringToFront,
9584
+ sendToBack,
9585
+ bringForward,
9586
+ sendBackward,
9587
+ deleteElement
9588
+ } = usePlayerManager({ videoProps, canvasConfig });
9589
+ const [contextMenu, setContextMenu] = React.useState(null);
9590
+ const closeContextMenu = React.useCallback(() => setContextMenu(null), []);
9591
+ const { handleDragOver, handleDragLeave, handleDrop } = useCanvasDrop({
9592
+ containerRef,
9593
+ videoSize: { width: videoProps.width, height: videoProps.height },
9594
+ onDrop: handleDropOnCanvas,
9595
+ enabled: !!handleDropOnCanvas && canvasMode
9596
+ });
9597
+ React.useEffect(() => {
9598
+ const container = containerRef.current;
9599
+ const canvasSize = {
9600
+ width: container == null ? void 0 : container.clientWidth,
9601
+ height: container == null ? void 0 : container.clientHeight
9602
+ };
9603
+ buildCanvas({
9604
+ backgroundColor: videoProps.backgroundColor,
9605
+ videoSize: {
9606
+ width: videoProps.width,
9607
+ height: videoProps.height
9608
+ },
9609
+ canvasSize,
9610
+ canvasRef: canvasRef.current
9611
+ });
9612
+ }, [videoProps]);
9613
+ React.useEffect(() => {
9614
+ if (twickCanvas && playerState === livePlayer.PLAYER_STATE.PAUSED) {
9615
+ updateCanvas(seekTime);
9616
+ }
9617
+ }, [twickCanvas, playerState, seekTime, changeLog]);
9618
+ React.useEffect(() => {
9619
+ if (!twickCanvas || !canvasMode) return;
9620
+ const onSelectionCreated = (e3) => {
9621
+ var _a, _b;
9622
+ const ev = e3 == null ? void 0 : e3.e;
9623
+ if (!ev) return;
9624
+ const id2 = (_b = (_a = e3.target) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "id");
9625
+ if (id2) {
9626
+ setContextMenu({ x: ev.clientX, y: ev.clientY, elementId: id2 });
9627
+ }
9628
+ };
9629
+ twickCanvas.on("contextmenu", onSelectionCreated);
9630
+ return () => {
9631
+ twickCanvas.off("contextmenu", onSelectionCreated);
9632
+ };
9633
+ }, [twickCanvas, canvasMode]);
9634
+ const handleTimeUpdate = (time2) => {
9635
+ if (durationRef.current && time2 >= durationRef.current) {
9636
+ setCurrentTime(0);
9637
+ setPlayerState(livePlayer.PLAYER_STATE.PAUSED);
9638
+ } else {
9639
+ setCurrentTime(time2);
9640
+ }
9641
+ };
9642
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9643
+ "div",
9644
+ {
9645
+ className: "twick-editor-container",
9646
+ style: {
9647
+ aspectRatio: `${videoProps.width}/${videoProps.height}`
9648
+ },
9649
+ children: [
9650
+ /* @__PURE__ */ jsxRuntime.jsx(
9651
+ "div",
9652
+ {
9653
+ className: "twick-editor-loading-overlay",
9654
+ style: {
9655
+ display: playerUpdating ? "flex" : "none"
9656
+ },
9657
+ children: playerUpdating ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-editor-loading-spinner" }) : null
9658
+ }
9659
+ ),
9660
+ /* @__PURE__ */ jsxRuntime.jsx(
9661
+ livePlayer.LivePlayer,
9662
+ {
9663
+ seekTime,
9664
+ projectData,
9665
+ quality: (playerProps == null ? void 0 : playerProps.quality) || 1,
9666
+ videoSize: {
9667
+ width: videoProps.width,
9668
+ height: videoProps.height
9669
+ },
9670
+ onPlayerUpdate,
9671
+ containerStyle: {
9672
+ opacity: canvasMode ? playerState === livePlayer.PLAYER_STATE.PAUSED ? 0 : 1 : 1
9673
+ },
9674
+ onTimeUpdate: handleTimeUpdate,
9675
+ volume: playerVolume,
9676
+ onDurationChange: (duration) => {
9677
+ durationRef.current = duration;
9678
+ },
9679
+ playing: playerState === livePlayer.PLAYER_STATE.PLAYING
9680
+ }
9681
+ ),
9682
+ canvasMode && /* @__PURE__ */ jsxRuntime.jsx(
9683
+ "div",
9684
+ {
9685
+ ref: containerRef,
9686
+ className: "twick-editor-canvas-container",
9687
+ style: {
9688
+ opacity: playerState === livePlayer.PLAYER_STATE.PAUSED ? 1 : 0
9689
+ },
9690
+ onDragOver: handleDragOver,
9691
+ onDragLeave: handleDragLeave,
9692
+ onDrop: handleDrop,
9693
+ onContextMenu: (e3) => e3.preventDefault(),
9694
+ children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "twick-editor-canvas" })
9695
+ }
9696
+ ),
9697
+ contextMenu && /* @__PURE__ */ jsxRuntime.jsx(
9698
+ CanvasContextMenu,
9699
+ {
9700
+ x: contextMenu.x,
9701
+ y: contextMenu.y,
9702
+ elementId: contextMenu.elementId,
9703
+ onBringToFront: bringToFront,
9704
+ onSendToBack: sendToBack,
9705
+ onBringForward: bringForward,
9706
+ onSendBackward: sendBackward,
9707
+ onDelete: deleteElement,
9708
+ onClose: closeContextMenu
9709
+ }
9710
+ )
9711
+ ]
9712
+ }
9713
+ );
9714
+ };
9715
+ function clamp$1(v2, min, max) {
9716
+ return Math.max(min, Math.min(v2, max));
9717
+ }
9718
+ const V = {
9719
+ toVector(v2, fallback) {
9720
+ if (v2 === void 0) v2 = fallback;
9721
+ return Array.isArray(v2) ? v2 : [v2, v2];
8427
9722
  },
8428
9723
  add(v1, v2) {
8429
9724
  return [v1[0] + v2[0], v1[1] + v2[1]];
@@ -9753,7 +11048,8 @@ function SeekTrack({
9753
11048
  zoom = 1,
9754
11049
  onSeek,
9755
11050
  timelineCount = 0,
9756
- timelineTickConfigs
11051
+ timelineTickConfigs,
11052
+ onPlayheadUpdate
9757
11053
  }) {
9758
11054
  const containerRef = React.useRef(null);
9759
11055
  const [isDragging2, setIsDragging] = React.useState(false);
@@ -9765,6 +11061,12 @@ function SeekTrack({
9765
11061
  const position = isDragging2 && dragPosition !== null ? dragPosition : currentTime * pixelsPerSecond;
9766
11062
  return Math.max(0, position);
9767
11063
  }, [isDragging2, dragPosition, currentTime, pixelsPerSecond]);
11064
+ React.useEffect(() => {
11065
+ onPlayheadUpdate == null ? void 0 : onPlayheadUpdate({
11066
+ positionPx: seekPosition,
11067
+ isDragging: isDragging2
11068
+ });
11069
+ }, [seekPosition, isDragging2, onPlayheadUpdate]);
9768
11070
  const { majorIntervalSec, minorIntervalSec } = React.useMemo(() => {
9769
11071
  if (timelineTickConfigs && timelineTickConfigs.length > 0) {
9770
11072
  const sortedConfigs = [...timelineTickConfigs].sort((a2, b2) => a2.durationThreshold - b2.durationThreshold);
@@ -9933,7 +11235,7 @@ function SeekTrack({
9933
11235
  transform: `translateX(${seekPosition}px)`,
9934
11236
  top: 0,
9935
11237
  touchAction: "none",
9936
- transition: isDragging2 ? "none" : "transform 0.1s linear",
11238
+ transition: isDragging2 ? "none" : "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)",
9937
11239
  willChange: isDragging2 ? "transform" : "auto"
9938
11240
  },
9939
11241
  children: [
@@ -9957,7 +11259,8 @@ const SeekControl = ({
9957
11259
  zoom,
9958
11260
  timelineCount,
9959
11261
  onSeek,
9960
- timelineTickConfigs
11262
+ timelineTickConfigs,
11263
+ onPlayheadUpdate
9961
11264
  }) => {
9962
11265
  const { currentTime } = livePlayer.useLivePlayerContext();
9963
11266
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -9968,7 +11271,8 @@ const SeekControl = ({
9968
11271
  zoom,
9969
11272
  onSeek,
9970
11273
  timelineCount,
9971
- timelineTickConfigs
11274
+ timelineTickConfigs,
11275
+ onPlayheadUpdate
9972
11276
  }
9973
11277
  );
9974
11278
  };
@@ -10077,7 +11381,21 @@ const createLucideIcon = (iconName, iconNode) => {
10077
11381
  * This source code is licensed under the ISC license.
10078
11382
  * See the LICENSE file in the root directory of this source tree.
10079
11383
  */
10080
- const __iconNode$b = [
11384
+ const __iconNode$e = [
11385
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
11386
+ ["line", { x1: "22", x2: "18", y1: "12", y2: "12", key: "l9bcsi" }],
11387
+ ["line", { x1: "6", x2: "2", y1: "12", y2: "12", key: "13hhkx" }],
11388
+ ["line", { x1: "12", x2: "12", y1: "6", y2: "2", key: "10w3f3" }],
11389
+ ["line", { x1: "12", x2: "12", y1: "22", y2: "18", key: "15g9kq" }]
11390
+ ];
11391
+ const Crosshair = createLucideIcon("crosshair", __iconNode$e);
11392
+ /**
11393
+ * @license lucide-react v0.511.0 - ISC
11394
+ *
11395
+ * This source code is licensed under the ISC license.
11396
+ * See the LICENSE file in the root directory of this source tree.
11397
+ */
11398
+ const __iconNode$d = [
10081
11399
  ["circle", { cx: "9", cy: "12", r: "1", key: "1vctgf" }],
10082
11400
  ["circle", { cx: "9", cy: "5", r: "1", key: "hp0tcf" }],
10083
11401
  ["circle", { cx: "9", cy: "19", r: "1", key: "fkjjf6" }],
@@ -10085,81 +11403,103 @@ const __iconNode$b = [
10085
11403
  ["circle", { cx: "15", cy: "5", r: "1", key: "19l28e" }],
10086
11404
  ["circle", { cx: "15", cy: "19", r: "1", key: "f4zoj3" }]
10087
11405
  ];
10088
- const GripVertical = createLucideIcon("grip-vertical", __iconNode$b);
11406
+ const GripVertical = createLucideIcon("grip-vertical", __iconNode$d);
10089
11407
  /**
10090
11408
  * @license lucide-react v0.511.0 - ISC
10091
11409
  *
10092
11410
  * This source code is licensed under the ISC license.
10093
11411
  * See the LICENSE file in the root directory of this source tree.
10094
11412
  */
10095
- const __iconNode$a = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
10096
- const LoaderCircle = createLucideIcon("loader-circle", __iconNode$a);
11413
+ const __iconNode$c = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
11414
+ const LoaderCircle = createLucideIcon("loader-circle", __iconNode$c);
10097
11415
  /**
10098
11416
  * @license lucide-react v0.511.0 - ISC
10099
11417
  *
10100
11418
  * This source code is licensed under the ISC license.
10101
11419
  * See the LICENSE file in the root directory of this source tree.
10102
11420
  */
10103
- const __iconNode$9 = [
11421
+ const __iconNode$b = [
10104
11422
  ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
10105
11423
  ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4", key: "fwvmzm" }]
10106
11424
  ];
10107
- const Lock = createLucideIcon("lock", __iconNode$9);
11425
+ const Lock = createLucideIcon("lock", __iconNode$b);
10108
11426
  /**
10109
11427
  * @license lucide-react v0.511.0 - ISC
10110
11428
  *
10111
11429
  * This source code is licensed under the ISC license.
10112
11430
  * See the LICENSE file in the root directory of this source tree.
10113
11431
  */
10114
- const __iconNode$8 = [
11432
+ const __iconNode$a = [
10115
11433
  ["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
10116
11434
  ["rect", { x: "6", y: "4", width: "4", height: "16", rx: "1", key: "1okwgv" }]
10117
11435
  ];
10118
- const Pause = createLucideIcon("pause", __iconNode$8);
11436
+ const Pause = createLucideIcon("pause", __iconNode$a);
10119
11437
  /**
10120
11438
  * @license lucide-react v0.511.0 - ISC
10121
11439
  *
10122
11440
  * This source code is licensed under the ISC license.
10123
11441
  * See the LICENSE file in the root directory of this source tree.
10124
11442
  */
10125
- const __iconNode$7 = [["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]];
10126
- const Play = createLucideIcon("play", __iconNode$7);
11443
+ const __iconNode$9 = [["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]];
11444
+ const Play = createLucideIcon("play", __iconNode$9);
10127
11445
  /**
10128
11446
  * @license lucide-react v0.511.0 - ISC
10129
11447
  *
10130
11448
  * This source code is licensed under the ISC license.
10131
11449
  * See the LICENSE file in the root directory of this source tree.
10132
11450
  */
10133
- const __iconNode$6 = [
11451
+ const __iconNode$8 = [
10134
11452
  ["path", { d: "M5 12h14", key: "1ays0h" }],
10135
11453
  ["path", { d: "M12 5v14", key: "s699le" }]
10136
11454
  ];
10137
- const Plus = createLucideIcon("plus", __iconNode$6);
11455
+ const Plus = createLucideIcon("plus", __iconNode$8);
10138
11456
  /**
10139
11457
  * @license lucide-react v0.511.0 - ISC
10140
11458
  *
10141
11459
  * This source code is licensed under the ISC license.
10142
11460
  * See the LICENSE file in the root directory of this source tree.
10143
11461
  */
10144
- const __iconNode$5 = [
11462
+ const __iconNode$7 = [
10145
11463
  ["path", { d: "m15 14 5-5-5-5", key: "12vg1m" }],
10146
11464
  ["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" }]
10147
11465
  ];
10148
- const Redo2 = createLucideIcon("redo-2", __iconNode$5);
11466
+ const Redo2 = createLucideIcon("redo-2", __iconNode$7);
10149
11467
  /**
10150
11468
  * @license lucide-react v0.511.0 - ISC
10151
11469
  *
10152
11470
  * This source code is licensed under the ISC license.
10153
11471
  * See the LICENSE file in the root directory of this source tree.
10154
11472
  */
10155
- const __iconNode$4 = [
11473
+ const __iconNode$6 = [
10156
11474
  ["circle", { cx: "6", cy: "6", r: "3", key: "1lh9wr" }],
10157
11475
  ["path", { d: "M8.12 8.12 12 12", key: "1alkpv" }],
10158
11476
  ["path", { d: "M20 4 8.12 15.88", key: "xgtan2" }],
10159
11477
  ["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
10160
11478
  ["path", { d: "M14.8 14.8 20 20", key: "ptml3r" }]
10161
11479
  ];
10162
- const Scissors = createLucideIcon("scissors", __iconNode$4);
11480
+ const Scissors = createLucideIcon("scissors", __iconNode$6);
11481
+ /**
11482
+ * @license lucide-react v0.511.0 - ISC
11483
+ *
11484
+ * This source code is licensed under the ISC license.
11485
+ * See the LICENSE file in the root directory of this source tree.
11486
+ */
11487
+ const __iconNode$5 = [
11488
+ ["polygon", { points: "19 20 9 12 19 4 19 20", key: "o2sva" }],
11489
+ ["line", { x1: "5", x2: "5", y1: "19", y2: "5", key: "1ocqjk" }]
11490
+ ];
11491
+ const SkipBack = createLucideIcon("skip-back", __iconNode$5);
11492
+ /**
11493
+ * @license lucide-react v0.511.0 - ISC
11494
+ *
11495
+ * This source code is licensed under the ISC license.
11496
+ * See the LICENSE file in the root directory of this source tree.
11497
+ */
11498
+ const __iconNode$4 = [
11499
+ ["polygon", { points: "5 4 15 12 5 20 5 4", key: "16p6eg" }],
11500
+ ["line", { x1: "19", x2: "19", y1: "5", y2: "19", key: "futhcm" }]
11501
+ ];
11502
+ const SkipForward = createLucideIcon("skip-forward", __iconNode$4);
10163
11503
  /**
10164
11504
  * @license lucide-react v0.511.0 - ISC
10165
11505
  *
@@ -10212,7 +11552,7 @@ const __iconNode = [
10212
11552
  const ZoomOut = createLucideIcon("zoom-out", __iconNode);
10213
11553
  const TrackHeader = ({
10214
11554
  track,
10215
- selectedItem,
11555
+ selectedIds,
10216
11556
  onDragStart,
10217
11557
  onDragOver,
10218
11558
  onDrop,
@@ -10221,9 +11561,9 @@ const TrackHeader = ({
10221
11561
  return /* @__PURE__ */ jsxRuntime.jsxs(
10222
11562
  "div",
10223
11563
  {
10224
- className: `twick-track-header ${selectedItem instanceof timeline.Track && selectedItem.getId() === track.getId() ? "twick-track-header-selected" : "twick-track-header-default"}`,
11564
+ className: `twick-track-header ${selectedIds.has(track.getId()) ? "twick-track-header-selected" : "twick-track-header-default"}`,
10225
11565
  draggable: true,
10226
- onClick: () => onSelect(track),
11566
+ onClick: (e3) => onSelect(track, e3),
10227
11567
  onDragStart: (e3) => onDragStart(e3, track),
10228
11568
  onDragOver,
10229
11569
  onDrop: (e3) => onDrop(e3, track),
@@ -17260,321 +18600,124 @@ class VisualElement {
17260
18600
  }
17261
18601
  const target = this.getBaseTargetFromProps(this.props, key);
17262
18602
  if (target !== void 0 && !isMotionValue(target))
17263
- return target;
17264
- return this.initialValues[key] !== void 0 && valueFromInitial === void 0 ? void 0 : this.baseTarget[key];
17265
- }
17266
- on(eventName, callback) {
17267
- if (!this.events[eventName]) {
17268
- this.events[eventName] = new SubscriptionManager();
17269
- }
17270
- return this.events[eventName].add(callback);
17271
- }
17272
- notify(eventName, ...args) {
17273
- if (this.events[eventName]) {
17274
- this.events[eventName].notify(...args);
17275
- }
17276
- }
17277
- }
17278
- class DOMVisualElement extends VisualElement {
17279
- constructor() {
17280
- super(...arguments);
17281
- this.KeyframeResolver = DOMKeyframesResolver;
17282
- }
17283
- sortInstanceNodePosition(a2, b2) {
17284
- return a2.compareDocumentPosition(b2) & 2 ? 1 : -1;
17285
- }
17286
- getBaseTargetFromProps(props, key) {
17287
- return props.style ? props.style[key] : void 0;
17288
- }
17289
- removeValueFromRenderState(key, { vars, style }) {
17290
- delete vars[key];
17291
- delete style[key];
17292
- }
17293
- handleChildMotionValue() {
17294
- if (this.childSubscription) {
17295
- this.childSubscription();
17296
- delete this.childSubscription;
17297
- }
17298
- const { children } = this.props;
17299
- if (isMotionValue(children)) {
17300
- this.childSubscription = children.on("change", (latest) => {
17301
- if (this.current) {
17302
- this.current.textContent = `${latest}`;
17303
- }
17304
- });
17305
- }
17306
- }
17307
- }
17308
- function getComputedStyle(element) {
17309
- return window.getComputedStyle(element);
17310
- }
17311
- class HTMLVisualElement extends DOMVisualElement {
17312
- constructor() {
17313
- super(...arguments);
17314
- this.type = "html";
17315
- this.renderInstance = renderHTML;
17316
- }
17317
- readValueFromInstance(instance, key) {
17318
- if (transformProps.has(key)) {
17319
- const defaultType = getDefaultValueType(key);
17320
- return defaultType ? defaultType.default || 0 : 0;
17321
- } else {
17322
- const computedStyle = getComputedStyle(instance);
17323
- const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) : computedStyle[key]) || 0;
17324
- return typeof value === "string" ? value.trim() : value;
17325
- }
17326
- }
17327
- measureInstanceViewportBox(instance, { transformPagePoint }) {
17328
- return measureViewportBox(instance, transformPagePoint);
17329
- }
17330
- build(renderState, latestValues, props) {
17331
- buildHTMLStyles(renderState, latestValues, props.transformTemplate);
17332
- }
17333
- scrapeMotionValuesFromProps(props, prevProps, visualElement) {
17334
- return scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
17335
- }
17336
- }
17337
- class SVGVisualElement extends DOMVisualElement {
17338
- constructor() {
17339
- super(...arguments);
17340
- this.type = "svg";
17341
- this.isSVGTag = false;
17342
- this.measureInstanceViewportBox = createBox;
18603
+ return target;
18604
+ return this.initialValues[key] !== void 0 && valueFromInitial === void 0 ? void 0 : this.baseTarget[key];
17343
18605
  }
17344
- getBaseTargetFromProps(props, key) {
17345
- return props[key];
18606
+ on(eventName, callback) {
18607
+ if (!this.events[eventName]) {
18608
+ this.events[eventName] = new SubscriptionManager();
18609
+ }
18610
+ return this.events[eventName].add(callback);
17346
18611
  }
17347
- readValueFromInstance(instance, key) {
17348
- if (transformProps.has(key)) {
17349
- const defaultType = getDefaultValueType(key);
17350
- return defaultType ? defaultType.default || 0 : 0;
18612
+ notify(eventName, ...args) {
18613
+ if (this.events[eventName]) {
18614
+ this.events[eventName].notify(...args);
17351
18615
  }
17352
- key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
17353
- return instance.getAttribute(key);
17354
18616
  }
17355
- scrapeMotionValuesFromProps(props, prevProps, visualElement) {
17356
- return scrapeMotionValuesFromProps(props, prevProps, visualElement);
18617
+ }
18618
+ class DOMVisualElement extends VisualElement {
18619
+ constructor() {
18620
+ super(...arguments);
18621
+ this.KeyframeResolver = DOMKeyframesResolver;
17357
18622
  }
17358
- build(renderState, latestValues, props) {
17359
- buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate);
18623
+ sortInstanceNodePosition(a2, b2) {
18624
+ return a2.compareDocumentPosition(b2) & 2 ? 1 : -1;
17360
18625
  }
17361
- renderInstance(instance, renderState, styleProp, projection) {
17362
- renderSVG(instance, renderState, styleProp, projection);
18626
+ getBaseTargetFromProps(props, key) {
18627
+ return props.style ? props.style[key] : void 0;
17363
18628
  }
17364
- mount(instance) {
17365
- this.isSVGTag = isSVGTag(instance.tagName);
17366
- super.mount(instance);
18629
+ removeValueFromRenderState(key, { vars, style }) {
18630
+ delete vars[key];
18631
+ delete style[key];
17367
18632
  }
17368
- }
17369
- const createDomVisualElement = (Component, options) => {
17370
- return isSVGComponent(Component) ? new SVGVisualElement(options) : new HTMLVisualElement(options, {
17371
- allowProjection: Component !== React.Fragment
17372
- });
17373
- };
17374
- const createMotionComponent = /* @__PURE__ */ createMotionComponentFactory({
17375
- ...animations,
17376
- ...gestureAnimations,
17377
- ...drag,
17378
- ...layout
17379
- }, createDomVisualElement);
17380
- const motion = /* @__PURE__ */ createDOMMotionComponentProxy(createMotionComponent);
17381
- const INITIAL_TIMELINE_DATA = {
17382
- tracks: [
17383
- {
17384
- type: "element",
17385
- id: "t-sample",
17386
- name: "sample",
17387
- elements: [
17388
- {
17389
- id: "e-sample",
17390
- trackId: "t-sample",
17391
- name: "sample",
17392
- type: "text",
17393
- s: 0,
17394
- e: 5,
17395
- props: {
17396
- text: "Twick Video Editor",
17397
- fill: "#FFFFFF"
17398
- }
17399
- }
17400
- ]
18633
+ handleChildMotionValue() {
18634
+ if (this.childSubscription) {
18635
+ this.childSubscription();
18636
+ delete this.childSubscription;
17401
18637
  }
17402
- ],
17403
- version: 1
17404
- };
17405
- const MIN_DURATION = 0.1;
17406
- const DRAG_TYPE = {
17407
- /** Drag operation is starting */
17408
- START: "start",
17409
- /** Drag operation is in progress */
17410
- MOVE: "move",
17411
- /** Drag operation has ended */
17412
- END: "end"
17413
- };
17414
- const DEFAULT_TIMELINE_ZOOM = 1.5;
17415
- const DEFAULT_TIMELINE_ZOOM_CONFIG = {
17416
- /** Minimum zoom level (10%) */
17417
- min: 0.1,
17418
- /** Maximum zoom level (300%) */
17419
- max: 3,
17420
- /** Zoom step increment/decrement (10%) */
17421
- step: 0.1,
17422
- /** Default zoom level (150%) */
17423
- default: 1.5
17424
- };
17425
- const DEFAULT_TIMELINE_TICK_CONFIGS = [
17426
- {
17427
- durationThreshold: 10,
17428
- // < 10 seconds
17429
- majorInterval: 1,
17430
- // 1s major ticks
17431
- minorTicks: 10
17432
- // 0.1s minor ticks (10 minors between majors)
17433
- },
17434
- {
17435
- durationThreshold: 30,
17436
- // < 30 seconds
17437
- majorInterval: 5,
17438
- // 5s major ticks
17439
- minorTicks: 5
17440
- // 1s minor ticks (5 minors between majors)
17441
- },
17442
- {
17443
- durationThreshold: 120,
17444
- // < 2 minutes
17445
- majorInterval: 10,
17446
- // 10s major ticks
17447
- minorTicks: 5
17448
- // 2s minor ticks (5 minors between majors)
17449
- },
17450
- {
17451
- durationThreshold: 300,
17452
- // < 5 minutes
17453
- majorInterval: 30,
17454
- // 30s major ticks
17455
- minorTicks: 6
17456
- // 5s minor ticks (6 minors between majors)
17457
- },
17458
- {
17459
- durationThreshold: 900,
17460
- // < 15 minutes
17461
- majorInterval: 60,
17462
- // 1m major ticks
17463
- minorTicks: 6
17464
- // 10s minor ticks (6 minors between majors)
17465
- },
17466
- {
17467
- durationThreshold: 1800,
17468
- // < 30 minutes
17469
- majorInterval: 120,
17470
- // 2m major ticks
17471
- minorTicks: 4
17472
- // 30s minor ticks (4 minors between majors)
17473
- },
17474
- {
17475
- durationThreshold: 3600,
17476
- // < 1 hour
17477
- majorInterval: 300,
17478
- // 5m major ticks
17479
- minorTicks: 5
17480
- // 1m minor ticks (5 minors between majors)
17481
- },
17482
- {
17483
- durationThreshold: 7200,
17484
- // < 2 hours
17485
- majorInterval: 600,
17486
- // 10m major ticks
17487
- minorTicks: 10
17488
- // 1m minor ticks (10 minors between majors)
17489
- },
17490
- {
17491
- durationThreshold: Infinity,
17492
- // >= 2 hours
17493
- majorInterval: 1800,
17494
- // 30m major ticks
17495
- minorTicks: 6
17496
- // 5m minor ticks (6 minors between majors)
17497
- }
17498
- ];
17499
- const DEFAULT_ELEMENT_COLORS = {
17500
- /** Fragment element color - deep charcoal matching UI background */
17501
- fragment: "#1A1A1A",
17502
- /** Video element color - vibrant royal purple */
17503
- video: "#8B5FBF",
17504
- /** Caption element color - soft wisteria purple */
17505
- caption: "#9B8ACE",
17506
- /** Image element color - warm copper accent */
17507
- image: "#D4956C",
17508
- /** Audio element color - deep teal */
17509
- audio: "#3D8B8B",
17510
- /** Text element color - medium lavender */
17511
- text: "#8D74C4",
17512
- /** Generic element color - muted amethyst */
17513
- element: "#7B68B8",
17514
- /** Rectangle element color - deep indigo */
17515
- rect: "#5B4B99",
17516
- /** Frame effect color - rich magenta */
17517
- frameEffect: "#B55B9C",
17518
- /** Filters color - periwinkle blue */
17519
- filters: "#7A89D4",
17520
- /** Transition color - burnished bronze */
17521
- transition: "#BE8157",
17522
- /** Animation color - muted emerald */
17523
- animation: "#4B9B78",
17524
- /** Icon element color - bright orchid */
17525
- icon: "#A76CD4",
17526
- /** Circle element color - deep byzantium */
17527
- circle: "#703D8B"
17528
- };
17529
- const AVAILABLE_TEXT_FONTS = {
17530
- // Google Fonts
17531
- /** Modern sans-serif font */
17532
- RUBIK: "Rubik",
17533
- /** Clean and readable font */
17534
- MULISH: "Mulish",
17535
- /** Bold display font */
17536
- LUCKIEST_GUY: "Luckiest Guy",
17537
- /** Elegant serif font */
17538
- PLAYFAIR_DISPLAY: "Playfair Display",
17539
- /** Classic sans-serif font */
17540
- ROBOTO: "Roboto",
17541
- /** Modern geometric font */
17542
- POPPINS: "Poppins",
17543
- // Display and Decorative Fonts
17544
- /** Comic-style display font */
17545
- BANGERS: "Bangers",
17546
- /** Handwritten-style font */
17547
- BIRTHSTONE: "Birthstone",
17548
- /** Elegant script font */
17549
- CORINTHIA: "Corinthia",
17550
- /** Formal script font */
17551
- IMPERIAL_SCRIPT: "Imperial Script",
17552
- /** Bold outline font */
17553
- KUMAR_ONE_OUTLINE: "Kumar One Outline",
17554
- /** Light outline font */
17555
- LONDRI_OUTLINE: "Londrina Outline",
17556
- /** Casual script font */
17557
- MARCK_SCRIPT: "Marck Script",
17558
- /** Modern sans-serif font */
17559
- MONTSERRAT: "Montserrat",
17560
- /** Stylish display font */
17561
- PATTAYA: "Pattaya",
17562
- // CDN Fonts
17563
- /** Unique display font */
17564
- PERALTA: "Peralta",
17565
- /** Bold impact font */
17566
- IMPACT: "Impact",
17567
- /** Handwritten-style font */
17568
- LUMANOSIMO: "Lumanosimo",
17569
- /** Custom display font */
17570
- KAPAKANA: "Kapakana",
17571
- /** Handwritten font */
17572
- HANDYRUSH: "HandyRush",
17573
- /** Decorative font */
17574
- DASHER: "Dasher",
17575
- /** Signature-style font */
17576
- BRITTANY_SIGNATURE: "Brittany Signature"
18638
+ const { children } = this.props;
18639
+ if (isMotionValue(children)) {
18640
+ this.childSubscription = children.on("change", (latest) => {
18641
+ if (this.current) {
18642
+ this.current.textContent = `${latest}`;
18643
+ }
18644
+ });
18645
+ }
18646
+ }
18647
+ }
18648
+ function getComputedStyle(element) {
18649
+ return window.getComputedStyle(element);
18650
+ }
18651
+ class HTMLVisualElement extends DOMVisualElement {
18652
+ constructor() {
18653
+ super(...arguments);
18654
+ this.type = "html";
18655
+ this.renderInstance = renderHTML;
18656
+ }
18657
+ readValueFromInstance(instance, key) {
18658
+ if (transformProps.has(key)) {
18659
+ const defaultType = getDefaultValueType(key);
18660
+ return defaultType ? defaultType.default || 0 : 0;
18661
+ } else {
18662
+ const computedStyle = getComputedStyle(instance);
18663
+ const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) : computedStyle[key]) || 0;
18664
+ return typeof value === "string" ? value.trim() : value;
18665
+ }
18666
+ }
18667
+ measureInstanceViewportBox(instance, { transformPagePoint }) {
18668
+ return measureViewportBox(instance, transformPagePoint);
18669
+ }
18670
+ build(renderState, latestValues, props) {
18671
+ buildHTMLStyles(renderState, latestValues, props.transformTemplate);
18672
+ }
18673
+ scrapeMotionValuesFromProps(props, prevProps, visualElement) {
18674
+ return scrapeMotionValuesFromProps$1(props, prevProps, visualElement);
18675
+ }
18676
+ }
18677
+ class SVGVisualElement extends DOMVisualElement {
18678
+ constructor() {
18679
+ super(...arguments);
18680
+ this.type = "svg";
18681
+ this.isSVGTag = false;
18682
+ this.measureInstanceViewportBox = createBox;
18683
+ }
18684
+ getBaseTargetFromProps(props, key) {
18685
+ return props[key];
18686
+ }
18687
+ readValueFromInstance(instance, key) {
18688
+ if (transformProps.has(key)) {
18689
+ const defaultType = getDefaultValueType(key);
18690
+ return defaultType ? defaultType.default || 0 : 0;
18691
+ }
18692
+ key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
18693
+ return instance.getAttribute(key);
18694
+ }
18695
+ scrapeMotionValuesFromProps(props, prevProps, visualElement) {
18696
+ return scrapeMotionValuesFromProps(props, prevProps, visualElement);
18697
+ }
18698
+ build(renderState, latestValues, props) {
18699
+ buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate);
18700
+ }
18701
+ renderInstance(instance, renderState, styleProp, projection) {
18702
+ renderSVG(instance, renderState, styleProp, projection);
18703
+ }
18704
+ mount(instance) {
18705
+ this.isSVGTag = isSVGTag(instance.tagName);
18706
+ super.mount(instance);
18707
+ }
18708
+ }
18709
+ const createDomVisualElement = (Component, options) => {
18710
+ return isSVGComponent(Component) ? new SVGVisualElement(options) : new HTMLVisualElement(options, {
18711
+ allowProjection: Component !== React.Fragment
18712
+ });
17577
18713
  };
18714
+ const createMotionComponent = /* @__PURE__ */ createMotionComponentFactory({
18715
+ ...animations,
18716
+ ...gestureAnimations,
18717
+ ...drag,
18718
+ ...layout
18719
+ }, createDomVisualElement);
18720
+ const motion = /* @__PURE__ */ createDOMMotionComponentProxy(createMotionComponent);
17578
18721
  let ELEMENT_COLORS = { ...DEFAULT_ELEMENT_COLORS };
17579
18722
  const setElementColors = (colors) => {
17580
18723
  ELEMENT_COLORS = {
@@ -17589,6 +18732,7 @@ const TrackElementView = ({
17589
18732
  nextStart,
17590
18733
  prevEnd,
17591
18734
  selectedItem,
18735
+ selectedIds,
17592
18736
  onSelection,
17593
18737
  onDrag,
17594
18738
  allowOverlap = false,
@@ -17615,8 +18759,8 @@ const TrackElementView = ({
17615
18759
  dragType.current = DRAG_TYPE.MOVE;
17616
18760
  setPosition((prev) => {
17617
18761
  const span = prev.end - prev.start;
17618
- let newStart = Math.max(0, prev.start + dx / parentWidth * duration);
17619
- newStart = Math.min(newStart, prev.end - MIN_DURATION);
18762
+ let newStart = prev.start + dx / parentWidth * duration;
18763
+ newStart = Math.max(0, Math.min(newStart, prev.end - MIN_DURATION));
17620
18764
  if (!allowOverlap) {
17621
18765
  if (prevEnd !== null && newStart < prevEnd) {
17622
18766
  newStart = prevEnd;
@@ -17640,8 +18784,8 @@ const TrackElementView = ({
17640
18784
  }
17641
18785
  dragType.current = DRAG_TYPE.START;
17642
18786
  setPosition((prev) => {
17643
- let newStart = Math.max(0, prev.start + dx / parentWidth * duration);
17644
- newStart = Math.min(newStart, prev.end - MIN_DURATION);
18787
+ let newStart = prev.start + dx / parentWidth * duration;
18788
+ newStart = Math.max(0, Math.min(newStart, prev.end - MIN_DURATION));
17645
18789
  if (prevEnd !== null && !allowOverlap && newStart < prevEnd) {
17646
18790
  newStart = prevEnd;
17647
18791
  }
@@ -17699,8 +18843,9 @@ const TrackElementView = ({
17699
18843
  return ELEMENT_COLORS.element;
17700
18844
  };
17701
18845
  const isSelected = React.useMemo(() => {
17702
- return (selectedItem == null ? void 0 : selectedItem.getId()) === element.getId();
17703
- }, [selectedItem, element]);
18846
+ return selectedIds.has(element.getId());
18847
+ }, [selectedIds, element]);
18848
+ const hasHandles = (selectedItem == null ? void 0 : selectedItem.getId()) === element.getId();
17704
18849
  const motionProps = {
17705
18850
  ref,
17706
18851
  className: `twick-track-element ${isSelected ? "twick-track-element-selected" : "twick-track-element-default"} ${isDragging2 ? "twick-track-element-dragging" : ""}`,
@@ -17716,9 +18861,9 @@ const TrackElementView = ({
17716
18861
  },
17717
18862
  onMouseUp: sendUpdate,
17718
18863
  onTouchEnd: sendUpdate,
17719
- onClick: () => {
18864
+ onClick: (e3) => {
17720
18865
  if (onSelection) {
17721
- onSelection(element);
18866
+ onSelection(element, e3);
17722
18867
  }
17723
18868
  },
17724
18869
  style: {
@@ -17729,7 +18874,7 @@ const TrackElementView = ({
17729
18874
  }
17730
18875
  };
17731
18876
  return /* @__PURE__ */ jsxRuntime.jsx(motion.div, { ...motionProps, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { touchAction: "none", height: "100%" }, ...bind(), children: [
17732
- isSelected ? /* @__PURE__ */ jsxRuntime.jsx(
18877
+ hasHandles ? /* @__PURE__ */ jsxRuntime.jsx(
17733
18878
  "div",
17734
18879
  {
17735
18880
  style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
@@ -17738,7 +18883,7 @@ const TrackElementView = ({
17738
18883
  }
17739
18884
  ) : null,
17740
18885
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-track-element-content", children: element.getText ? element.getText() : element.getName() || element.getType() }),
17741
- isSelected ? /* @__PURE__ */ jsxRuntime.jsx(
18886
+ hasHandles ? /* @__PURE__ */ jsxRuntime.jsx(
17742
18887
  "div",
17743
18888
  {
17744
18889
  style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
@@ -17768,6 +18913,7 @@ const TrackBase = ({
17768
18913
  track,
17769
18914
  trackWidth,
17770
18915
  selectedItem,
18916
+ selectedIds,
17771
18917
  onItemSelection,
17772
18918
  onDrag,
17773
18919
  allowOverlap = false,
@@ -17792,6 +18938,7 @@ const TrackBase = ({
17792
18938
  allowOverlap,
17793
18939
  parentWidth: trackWidth,
17794
18940
  selectedItem,
18941
+ selectedIds,
17795
18942
  onSelection: onItemSelection,
17796
18943
  onDrag,
17797
18944
  elementColors,
@@ -17803,6 +18950,199 @@ const TrackBase = ({
17803
18950
  }
17804
18951
  );
17805
18952
  };
18953
+ const DEFAULT_MARGIN = 80;
18954
+ function usePlayheadScroll(scrollContainerRef, playheadPositionPx, isActive, config) {
18955
+ const margin = (config == null ? void 0 : config.margin) ?? DEFAULT_MARGIN;
18956
+ const labelWidth = config == null ? void 0 : config.labelWidth;
18957
+ const rafRef = React.useRef(null);
18958
+ React.useEffect(() => {
18959
+ if (!isActive || !scrollContainerRef.current) return;
18960
+ const container = scrollContainerRef.current;
18961
+ const contentX = labelWidth + playheadPositionPx;
18962
+ const scrollToKeepPlayheadVisible = () => {
18963
+ const { scrollLeft, clientWidth } = container;
18964
+ const minVisible = scrollLeft + margin;
18965
+ const maxVisible = scrollLeft + clientWidth - margin;
18966
+ let newScrollLeft = null;
18967
+ if (contentX < minVisible) {
18968
+ newScrollLeft = Math.max(0, contentX - margin);
18969
+ } else if (contentX > maxVisible) {
18970
+ newScrollLeft = contentX - clientWidth + margin;
18971
+ }
18972
+ if (newScrollLeft !== null) {
18973
+ container.scrollLeft = newScrollLeft;
18974
+ }
18975
+ };
18976
+ const scheduleScroll = () => {
18977
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
18978
+ rafRef.current = requestAnimationFrame(() => {
18979
+ rafRef.current = null;
18980
+ scrollToKeepPlayheadVisible();
18981
+ });
18982
+ };
18983
+ scheduleScroll();
18984
+ return () => {
18985
+ if (rafRef.current !== null) {
18986
+ cancelAnimationFrame(rafRef.current);
18987
+ }
18988
+ };
18989
+ }, [
18990
+ isActive,
18991
+ playheadPositionPx,
18992
+ scrollContainerRef,
18993
+ margin,
18994
+ labelWidth
18995
+ ]);
18996
+ }
18997
+ const MARQUEE_THRESHOLD = 4;
18998
+ function useMarqueeSelection({
18999
+ duration,
19000
+ zoomLevel,
19001
+ labelWidth,
19002
+ trackCount,
19003
+ trackHeight,
19004
+ tracks,
19005
+ containerRef,
19006
+ onMarqueeSelect,
19007
+ onEmptyClick
19008
+ }) {
19009
+ const [marquee, setMarquee] = React.useState(null);
19010
+ const startPosRef = React.useRef(null);
19011
+ const hasDraggedRef = React.useRef(false);
19012
+ const marqueeRef = React.useRef(marquee);
19013
+ marqueeRef.current = marquee;
19014
+ const pixelsPerSecond = 100 * zoomLevel;
19015
+ const getCoords = React.useCallback(
19016
+ (e3) => {
19017
+ var _a, _b;
19018
+ const rect = (_a = containerRef.current) == null ? void 0 : _a.getBoundingClientRect();
19019
+ if (!rect) return { x: 0, y: 0 };
19020
+ return {
19021
+ x: e3.clientX - rect.left + (((_b = containerRef.current) == null ? void 0 : _b.scrollLeft) ?? 0),
19022
+ y: e3.clientY - rect.top
19023
+ };
19024
+ },
19025
+ [containerRef]
19026
+ );
19027
+ const handleGlobalMouseMove = React.useCallback(
19028
+ (e3) => {
19029
+ if (!startPosRef.current) return;
19030
+ const { x: x2, y: y2 } = getCoords(e3);
19031
+ const dx = Math.abs(x2 - startPosRef.current.x);
19032
+ const dy = Math.abs(y2 - startPosRef.current.y);
19033
+ if (dx > MARQUEE_THRESHOLD || dy > MARQUEE_THRESHOLD) {
19034
+ hasDraggedRef.current = true;
19035
+ }
19036
+ setMarquee((prev) => prev ? { ...prev, endX: x2, endY: y2 } : null);
19037
+ },
19038
+ [getCoords]
19039
+ );
19040
+ const handleGlobalMouseUp = React.useCallback(() => {
19041
+ if (!startPosRef.current) return;
19042
+ const currentMarquee = marqueeRef.current;
19043
+ if (!hasDraggedRef.current || !currentMarquee) {
19044
+ setMarquee(null);
19045
+ startPosRef.current = null;
19046
+ onEmptyClick();
19047
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
19048
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
19049
+ return;
19050
+ }
19051
+ const left = Math.min(currentMarquee.startX, currentMarquee.endX);
19052
+ const right = Math.max(currentMarquee.startX, currentMarquee.endX);
19053
+ const top = Math.min(currentMarquee.startY, currentMarquee.endY);
19054
+ const bottom = Math.max(currentMarquee.startY, currentMarquee.endY);
19055
+ const startTime = Math.max(0, (left - labelWidth) / pixelsPerSecond);
19056
+ const endTime = Math.min(duration, (right - labelWidth) / pixelsPerSecond);
19057
+ const rowHeight = trackHeight + 2;
19058
+ const startTrackIdx = Math.max(0, Math.floor(top / rowHeight));
19059
+ const endTrackIdx = Math.min(
19060
+ trackCount - 1,
19061
+ Math.floor(bottom / rowHeight)
19062
+ );
19063
+ const selectedIds = /* @__PURE__ */ new Set();
19064
+ for (let tIdx = startTrackIdx; tIdx <= endTrackIdx; tIdx++) {
19065
+ const track = tracks[tIdx];
19066
+ if (!track) continue;
19067
+ for (const el of track.getElements()) {
19068
+ const elStart = el.getStart();
19069
+ const elEnd = el.getEnd();
19070
+ if (elStart < endTime && elEnd > startTime) {
19071
+ selectedIds.add(el.getId());
19072
+ }
19073
+ }
19074
+ }
19075
+ onMarqueeSelect(selectedIds);
19076
+ setMarquee(null);
19077
+ startPosRef.current = null;
19078
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
19079
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
19080
+ }, [
19081
+ duration,
19082
+ pixelsPerSecond,
19083
+ labelWidth,
19084
+ trackCount,
19085
+ trackHeight,
19086
+ tracks,
19087
+ onMarqueeSelect,
19088
+ onEmptyClick,
19089
+ handleGlobalMouseMove
19090
+ ]);
19091
+ React.useEffect(() => {
19092
+ if (!marquee) return;
19093
+ window.addEventListener("mousemove", handleGlobalMouseMove);
19094
+ window.addEventListener("mouseup", handleGlobalMouseUp);
19095
+ return () => {
19096
+ window.removeEventListener("mousemove", handleGlobalMouseMove);
19097
+ window.removeEventListener("mouseup", handleGlobalMouseUp);
19098
+ };
19099
+ }, [marquee, handleGlobalMouseMove, handleGlobalMouseUp]);
19100
+ const handleMouseDown = React.useCallback(
19101
+ (e3) => {
19102
+ if (e3.target.closest(".twick-track-element") || e3.target.closest(".twick-track-header")) {
19103
+ return;
19104
+ }
19105
+ const { x: x2, y: y2 } = getCoords(e3.nativeEvent);
19106
+ startPosRef.current = { x: x2, y: y2 };
19107
+ hasDraggedRef.current = false;
19108
+ setMarquee({ startX: x2, startY: y2, endX: x2, endY: y2 });
19109
+ },
19110
+ [getCoords]
19111
+ );
19112
+ return { marquee, handleMouseDown };
19113
+ }
19114
+ function MarqueeOverlay({ marquee }) {
19115
+ return /* @__PURE__ */ jsxRuntime.jsx(
19116
+ "div",
19117
+ {
19118
+ className: "twick-marquee-overlay",
19119
+ style: {
19120
+ position: "absolute",
19121
+ inset: 0,
19122
+ zIndex: 25,
19123
+ pointerEvents: "none"
19124
+ },
19125
+ children: marquee && /* @__PURE__ */ jsxRuntime.jsx(
19126
+ "div",
19127
+ {
19128
+ className: "twick-marquee-rect",
19129
+ style: {
19130
+ position: "absolute",
19131
+ left: Math.min(marquee.startX, marquee.endX),
19132
+ top: Math.min(marquee.startY, marquee.endY),
19133
+ width: Math.abs(marquee.endX - marquee.startX),
19134
+ height: Math.abs(marquee.endY - marquee.startY),
19135
+ border: "1px solid rgba(255, 255, 255, 0.7)",
19136
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
19137
+ pointerEvents: "none"
19138
+ }
19139
+ }
19140
+ )
19141
+ }
19142
+ );
19143
+ }
19144
+ const LABEL_WIDTH = 40;
19145
+ const TRACK_HEIGHT = 44;
17806
19146
  function TimelineView({
17807
19147
  zoomLevel,
17808
19148
  selectedItem,
@@ -17811,20 +19151,28 @@ function TimelineView({
17811
19151
  seekTrack,
17812
19152
  onAddTrack,
17813
19153
  onReorder,
17814
- onSelectionChange,
19154
+ onItemSelect,
19155
+ onEmptyClick,
19156
+ onMarqueeSelect,
17815
19157
  onElementDrag,
17816
- elementColors
19158
+ elementColors,
19159
+ selectedIds,
19160
+ playheadPositionPx = 0,
19161
+ isPlayheadActive = false,
19162
+ onDropOnTimeline,
19163
+ videoResolution,
19164
+ enableDropOnTimeline = true
17817
19165
  }) {
17818
19166
  const containerRef = React.useRef(null);
17819
19167
  const seekContainerRef = React.useRef(null);
17820
19168
  const timelineContentRef = React.useRef(null);
17821
19169
  const [, setScrollLeft] = React.useState(0);
17822
19170
  const [draggedTimeline, setDraggedTimeline] = React.useState(null);
17823
- const { selectedTrack, selectedTrackElement } = React.useMemo(() => {
19171
+ const { selectedTrackElement } = React.useMemo(() => {
17824
19172
  if (selectedItem && "elements" in selectedItem) {
17825
- return { selectedTrack: selectedItem, selectedTrackElement: null };
19173
+ return { selectedTrackElement: null };
17826
19174
  }
17827
- return { selectedTrack: null, selectedTrackElement: selectedItem };
19175
+ return { selectedTrackElement: selectedItem };
17828
19176
  }, [selectedItem]);
17829
19177
  const timelineWidth = Math.max(100, duration * zoomLevel * 100);
17830
19178
  const timelineWidthPx = `${timelineWidth}px`;
@@ -17854,7 +19202,33 @@ function TimelineView({
17854
19202
  window.removeEventListener("resize", updateWidth);
17855
19203
  };
17856
19204
  }, [duration, zoomLevel]);
17857
- const labelWidth = 140;
19205
+ usePlayheadScroll(containerRef, playheadPositionPx, isPlayheadActive, {
19206
+ labelWidth: LABEL_WIDTH
19207
+ });
19208
+ const { marquee, handleMouseDown: handleMarqueeMouseDown } = useMarqueeSelection({
19209
+ duration,
19210
+ zoomLevel,
19211
+ labelWidth: LABEL_WIDTH,
19212
+ trackCount: (tracks == null ? void 0 : tracks.length) ?? 0,
19213
+ trackHeight: TRACK_HEIGHT,
19214
+ tracks: tracks ?? [],
19215
+ containerRef: timelineContentRef,
19216
+ onMarqueeSelect,
19217
+ onEmptyClick
19218
+ });
19219
+ const { preview, handleDragOver, handleDragLeave, handleDrop } = useTimelineDrop({
19220
+ containerRef: timelineContentRef,
19221
+ scrollContainerRef: containerRef,
19222
+ tracks: tracks ?? [],
19223
+ duration,
19224
+ zoomLevel,
19225
+ labelWidth: LABEL_WIDTH,
19226
+ trackHeight: TRACK_HEIGHT,
19227
+ trackContentWidth: timelineWidth - LABEL_WIDTH,
19228
+ onDrop: onDropOnTimeline ?? (async () => {
19229
+ }),
19230
+ enabled: enableDropOnTimeline && !!onDropOnTimeline && !!videoResolution
19231
+ });
17858
19232
  const handleTrackDragStart = (e3, track) => {
17859
19233
  setDraggedTimeline(track);
17860
19234
  e3.dataTransfer.setData("application/json", JSON.stringify(track));
@@ -17886,10 +19260,8 @@ function TimelineView({
17886
19260
  }
17887
19261
  setDraggedTimeline(null);
17888
19262
  };
17889
- const handleItemSelection = (element) => {
17890
- if (onSelectionChange) {
17891
- onSelectionChange(element);
17892
- }
19263
+ const handleItemSelection = (item, event) => {
19264
+ onItemSelect(item, event);
17893
19265
  };
17894
19266
  return /* @__PURE__ */ jsxRuntime.jsxs(
17895
19267
  "div",
@@ -17902,44 +19274,106 @@ function TimelineView({
17902
19274
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-seek-track-empty-space", onClick: onAddTrack, children: /* @__PURE__ */ jsxRuntime.jsx(Plus, { color: "white", size: 20 }) }),
17903
19275
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flexGrow: 1 }, children: seekTrack })
17904
19276
  ] }) : null }),
17905
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: timelineContentRef, style: { width: timelineWidthPx }, children: (tracks || []).map((track) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twick-timeline-container", children: [
17906
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-timeline-header-container", children: /* @__PURE__ */ jsxRuntime.jsx(
17907
- TrackHeader,
17908
- {
17909
- track,
17910
- selectedItem: selectedTrack,
17911
- onSelect: handleItemSelection,
17912
- onDragStart: handleTrackDragStart,
17913
- onDragOver: handleTrackDragOver,
17914
- onDrop: handleTrackDrop
17915
- }
17916
- ) }),
17917
- /* @__PURE__ */ jsxRuntime.jsx(
17918
- TrackBase,
17919
- {
17920
- track,
17921
- duration,
17922
- selectedItem: selectedTrackElement,
17923
- zoom: zoomLevel,
17924
- allowOverlap: false,
17925
- trackWidth: timelineWidth - labelWidth,
17926
- onItemSelection: handleItemSelection,
17927
- onDrag: onElementDrag,
17928
- elementColors
17929
- }
17930
- )
17931
- ] }, track.getId())) })
19277
+ /* @__PURE__ */ jsxRuntime.jsxs(
19278
+ "div",
19279
+ {
19280
+ ref: timelineContentRef,
19281
+ style: { width: timelineWidthPx, position: "relative" },
19282
+ onMouseDown: handleMarqueeMouseDown,
19283
+ onDragOver: handleDragOver,
19284
+ onDragLeave: handleDragLeave,
19285
+ onDrop: handleDrop,
19286
+ children: [
19287
+ /* @__PURE__ */ jsxRuntime.jsx(MarqueeOverlay, { marquee }),
19288
+ preview && /* @__PURE__ */ jsxRuntime.jsx(
19289
+ "div",
19290
+ {
19291
+ className: "twick-drop-preview",
19292
+ style: {
19293
+ position: "absolute",
19294
+ left: LABEL_WIDTH + preview.timeSec / duration * (timelineWidth - LABEL_WIDTH),
19295
+ top: preview.trackIndex * TRACK_HEIGHT + 2,
19296
+ width: preview.widthPct / 100 * (timelineWidth - LABEL_WIDTH),
19297
+ height: TRACK_HEIGHT - 4
19298
+ }
19299
+ }
19300
+ ),
19301
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", zIndex: 10 }, children: (tracks || []).map((track) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twick-timeline-container", children: [
19302
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-timeline-header-container", children: /* @__PURE__ */ jsxRuntime.jsx(
19303
+ TrackHeader,
19304
+ {
19305
+ track,
19306
+ selectedIds,
19307
+ onSelect: handleItemSelection,
19308
+ onDragStart: handleTrackDragStart,
19309
+ onDragOver: handleTrackDragOver,
19310
+ onDrop: handleTrackDrop
19311
+ }
19312
+ ) }),
19313
+ /* @__PURE__ */ jsxRuntime.jsx(
19314
+ TrackBase,
19315
+ {
19316
+ track,
19317
+ duration,
19318
+ selectedItem: selectedTrackElement,
19319
+ selectedIds,
19320
+ zoom: zoomLevel,
19321
+ allowOverlap: false,
19322
+ trackWidth: timelineWidth - LABEL_WIDTH,
19323
+ onItemSelection: handleItemSelection,
19324
+ onDrag: onElementDrag,
19325
+ elementColors
19326
+ }
19327
+ )
19328
+ ] }, track.getId())) })
19329
+ ]
19330
+ }
19331
+ )
17932
19332
  ]
17933
19333
  }
17934
19334
  );
17935
19335
  }
17936
19336
  const useTimelineManager = () => {
17937
- const { selectedItem, changeLog, setSelectedItem, totalDuration, editor } = timeline.useTimelineContext();
19337
+ const { selectedItem, changeLog, setSelectedItem, totalDuration, editor, selectedIds } = timeline.useTimelineContext();
17938
19338
  const onElementDrag = ({
17939
19339
  element,
17940
19340
  dragType,
17941
19341
  updates
17942
19342
  }) => {
19343
+ var _a;
19344
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
19345
+ const duration = totalDuration;
19346
+ if (dragType === DRAG_TYPE.MOVE && selectedIds.has(element.getId()) && selectedIds.size > 1) {
19347
+ const resolved = timeline.resolveIds(selectedIds, tracks);
19348
+ const elements = resolved.filter((item) => item instanceof timeline.TrackElement);
19349
+ if (elements.length > 1) {
19350
+ const minStart = Math.min(...elements.map((el) => el.getStart()));
19351
+ const maxEnd = Math.max(...elements.map((el) => el.getEnd()));
19352
+ const delta = updates.start - element.getStart();
19353
+ const deltaMin = -minStart;
19354
+ const deltaMax = duration - maxEnd;
19355
+ const clampedDelta = Math.max(deltaMin, Math.min(deltaMax, delta));
19356
+ for (const el of elements) {
19357
+ const newStart = el.getStart() + clampedDelta;
19358
+ const newEnd = el.getEnd() + clampedDelta;
19359
+ if (el instanceof timeline.VideoElement || el instanceof timeline.AudioElement) {
19360
+ const elementProps = el.getProps();
19361
+ const startDelta = newStart - el.getStart() * ((elementProps == null ? void 0 : elementProps.playbackRate) || 1);
19362
+ if (el instanceof timeline.AudioElement) {
19363
+ el.setStartAt(el.getStartAt() + startDelta);
19364
+ } else {
19365
+ el.setStartAt(el.getStartAt() + startDelta);
19366
+ }
19367
+ }
19368
+ el.setStart(newStart);
19369
+ el.setEnd(newEnd);
19370
+ editor.updateElement(el);
19371
+ }
19372
+ setSelectedItem(element);
19373
+ editor.refresh();
19374
+ return;
19375
+ }
19376
+ }
17943
19377
  if (dragType === DRAG_TYPE.START) {
17944
19378
  if (element instanceof timeline.VideoElement || element instanceof timeline.AudioElement) {
17945
19379
  const elementProps = element.getProps();
@@ -17986,13 +19420,135 @@ const useTimelineManager = () => {
17986
19420
  totalDuration
17987
19421
  };
17988
19422
  };
19423
+ function useTimelineSelection() {
19424
+ const { editor, selectedIds, setSelection, setSelectedItem } = timeline.useTimelineContext();
19425
+ const handleItemSelect = React.useCallback(
19426
+ (item, event) => {
19427
+ var _a;
19428
+ const id2 = item.getId();
19429
+ const isMulti = event.metaKey || event.ctrlKey;
19430
+ const isRange = event.shiftKey;
19431
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
19432
+ if (isMulti) {
19433
+ setSelection((prev) => {
19434
+ const next = new Set(prev);
19435
+ if (next.has(id2)) {
19436
+ next.delete(id2);
19437
+ } else {
19438
+ next.add(id2);
19439
+ }
19440
+ return next;
19441
+ });
19442
+ return;
19443
+ }
19444
+ if (isRange) {
19445
+ const primaryId = selectedIds.size > 0 ? [...selectedIds][0] : null;
19446
+ if (!primaryId) {
19447
+ setSelectedItem(item);
19448
+ return;
19449
+ }
19450
+ const primary = timeline.resolveId(primaryId, tracks);
19451
+ if (!primary) {
19452
+ setSelectedItem(item);
19453
+ return;
19454
+ }
19455
+ if (item instanceof timeline.Track && primary instanceof timeline.Track) {
19456
+ const trackIds = tracks.map((t2) => t2.getId());
19457
+ const fromIdx = trackIds.indexOf(primary.getId());
19458
+ const toIdx = trackIds.indexOf(item.getId());
19459
+ if (fromIdx !== -1 && toIdx !== -1) {
19460
+ const [start, end] = fromIdx <= toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
19461
+ const rangeIds = new Set(
19462
+ trackIds.slice(start, end + 1)
19463
+ );
19464
+ setSelection((prev) => /* @__PURE__ */ new Set([...prev, ...rangeIds]));
19465
+ return;
19466
+ }
19467
+ }
19468
+ if (item instanceof timeline.TrackElement && primary instanceof timeline.TrackElement) {
19469
+ const track = editor.getTrackById(item.getTrackId());
19470
+ const primaryTrack = editor.getTrackById(primary.getTrackId());
19471
+ if (track && primaryTrack && track.getId() === primaryTrack.getId()) {
19472
+ const rangeIds = timeline.getElementIdsInRange(
19473
+ track,
19474
+ primary.getId(),
19475
+ item.getId()
19476
+ );
19477
+ setSelection((prev) => /* @__PURE__ */ new Set([...prev, ...rangeIds]));
19478
+ return;
19479
+ }
19480
+ }
19481
+ }
19482
+ setSelectedItem(item);
19483
+ },
19484
+ [editor, selectedIds, setSelection, setSelectedItem]
19485
+ );
19486
+ const handleEmptyClick = React.useCallback(() => {
19487
+ setSelectedItem(null);
19488
+ }, [setSelectedItem]);
19489
+ const handleMarqueeSelect = React.useCallback(
19490
+ (ids) => {
19491
+ setSelection(ids);
19492
+ },
19493
+ [setSelection]
19494
+ );
19495
+ return { handleItemSelect, handleEmptyClick, handleMarqueeSelect };
19496
+ }
17989
19497
  const TimelineManager = ({
17990
19498
  trackZoom,
17991
19499
  timelineTickConfigs,
17992
19500
  elementColors
17993
19501
  }) => {
17994
19502
  var _a;
17995
- const { timelineData, totalDuration, selectedItem, onAddTrack, onReorder, onElementDrag, onSeek, onSelectionChange } = useTimelineManager();
19503
+ const { playerState } = livePlayer.useLivePlayerContext();
19504
+ const { followPlayheadEnabled, editor, videoResolution, setSelectedItem } = timeline.useTimelineContext();
19505
+ const {
19506
+ timelineData,
19507
+ totalDuration,
19508
+ selectedItem,
19509
+ onAddTrack,
19510
+ onReorder,
19511
+ onElementDrag,
19512
+ onSeek
19513
+ } = useTimelineManager();
19514
+ const { selectedIds } = timeline.useTimelineContext();
19515
+ const { handleItemSelect, handleEmptyClick, handleMarqueeSelect } = useTimelineSelection();
19516
+ const [playheadState, setPlayheadState] = React.useState({
19517
+ positionPx: 0,
19518
+ isDragging: false
19519
+ });
19520
+ const handlePlayheadUpdate = React.useCallback((state) => {
19521
+ setPlayheadState(state);
19522
+ }, []);
19523
+ const isPlayheadActive = followPlayheadEnabled && playerState === livePlayer.PLAYER_STATE.PLAYING || playheadState.isDragging;
19524
+ const handleDropOnTimeline = React.useCallback(
19525
+ async (params) => {
19526
+ const { track, timeSec, type, url } = params;
19527
+ const element = createElementFromDrop(type, url, videoResolution);
19528
+ element.setStart(timeSec);
19529
+ const targetTrack = track ?? editor.addTrack(`Track_${Date.now()}`);
19530
+ const tryAdd = async (t2) => {
19531
+ var _a2;
19532
+ try {
19533
+ const result = await editor.addElementToTrack(t2, element);
19534
+ if (result) {
19535
+ setSelectedItem(element);
19536
+ return true;
19537
+ }
19538
+ } catch (err) {
19539
+ if (err instanceof timeline.ValidationError && ((_a2 = err.errors) == null ? void 0 : _a2.includes(timeline.VALIDATION_ERROR_CODE.COLLISION_ERROR))) {
19540
+ const newTrack = editor.addTrack(`Track_${Date.now()}`);
19541
+ return tryAdd(newTrack);
19542
+ }
19543
+ throw err;
19544
+ }
19545
+ return false;
19546
+ };
19547
+ await tryAdd(targetTrack);
19548
+ editor.refresh();
19549
+ },
19550
+ [editor, videoResolution, setSelectedItem]
19551
+ );
17996
19552
  return /* @__PURE__ */ jsxRuntime.jsx(
17997
19553
  TimelineView,
17998
19554
  {
@@ -18000,14 +19556,22 @@ const TimelineManager = ({
18000
19556
  zoomLevel: trackZoom,
18001
19557
  duration: totalDuration,
18002
19558
  selectedItem,
19559
+ selectedIds,
18003
19560
  onDeletion: () => {
18004
19561
  },
18005
19562
  onAddTrack,
18006
19563
  onReorder,
18007
19564
  onElementDrag,
18008
19565
  onSeek,
18009
- onSelectionChange,
19566
+ onItemSelect: handleItemSelect,
19567
+ onEmptyClick: handleEmptyClick,
19568
+ onMarqueeSelect: handleMarqueeSelect,
18010
19569
  elementColors,
19570
+ playheadPositionPx: playheadState.positionPx,
19571
+ isPlayheadActive,
19572
+ onDropOnTimeline: handleDropOnTimeline,
19573
+ videoResolution,
19574
+ enableDropOnTimeline: true,
18011
19575
  seekTrack: /* @__PURE__ */ jsxRuntime.jsx(
18012
19576
  SeekControl,
18013
19577
  {
@@ -18015,7 +19579,8 @@ const TimelineManager = ({
18015
19579
  zoom: trackZoom,
18016
19580
  onSeek,
18017
19581
  timelineCount: ((_a = timelineData == null ? void 0 : timelineData.tracks) == null ? void 0 : _a.length) ?? 0,
18018
- timelineTickConfigs
19582
+ timelineTickConfigs,
19583
+ onPlayheadUpdate: handlePlayheadUpdate
18019
19584
  }
18020
19585
  )
18021
19586
  }
@@ -18045,6 +19610,7 @@ const UndoRedoControls = ({ canUndo, canRedo, onUndo, onRedo }) => {
18045
19610
  };
18046
19611
  const PlayerControls = ({
18047
19612
  selectedItem,
19613
+ selectedIds = /* @__PURE__ */ new Set(),
18048
19614
  duration,
18049
19615
  currentTime,
18050
19616
  playerState,
@@ -18058,21 +19624,31 @@ const PlayerControls = ({
18058
19624
  zoomLevel = 1,
18059
19625
  setZoomLevel,
18060
19626
  className = "",
18061
- zoomConfig = DEFAULT_TIMELINE_ZOOM_CONFIG
19627
+ zoomConfig = DEFAULT_TIMELINE_ZOOM_CONFIG,
19628
+ fps = DEFAULT_FPS,
19629
+ onSeek,
19630
+ followPlayheadEnabled = true,
19631
+ onFollowPlayheadToggle
18062
19632
  }) => {
18063
19633
  const MAX_ZOOM = zoomConfig.max;
18064
19634
  const MIN_ZOOM = zoomConfig.min;
18065
19635
  const ZOOM_STEP = zoomConfig.step;
18066
- const formatTime = React.useCallback((time2) => {
18067
- const minutes = Math.floor(time2 / 60);
18068
- const seconds = Math.floor(time2 % 60);
18069
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
18070
- }, []);
19636
+ const formatTime = React.useCallback(
19637
+ (time2) => timeline.formatTimeWithFrames(time2, fps),
19638
+ [fps]
19639
+ );
19640
+ const handleSeekToStart = React.useCallback(() => {
19641
+ onSeek == null ? void 0 : onSeek(0);
19642
+ }, [onSeek]);
19643
+ const handleSeekToEnd = React.useCallback(() => {
19644
+ onSeek == null ? void 0 : onSeek(duration);
19645
+ }, [onSeek, duration]);
18071
19646
  const handleDelete = React.useCallback(() => {
18072
- if (selectedItem && onDelete) {
18073
- onDelete(selectedItem);
19647
+ if (selectedIds.size > 0 && onDelete) {
19648
+ onDelete();
18074
19649
  }
18075
- }, [selectedItem, onDelete]);
19650
+ }, [selectedIds.size, onDelete]);
19651
+ const hasSelection = selectedIds.size > 0;
18076
19652
  const handleSplit = React.useCallback(() => {
18077
19653
  if (selectedItem instanceof timeline.TrackElement && onSplit) {
18078
19654
  onSplit(selectedItem, currentTime);
@@ -18094,9 +19670,9 @@ const PlayerControls = ({
18094
19670
  "button",
18095
19671
  {
18096
19672
  onClick: handleDelete,
18097
- disabled: !selectedItem,
19673
+ disabled: !hasSelection,
18098
19674
  title: "Delete",
18099
- className: `control-btn delete-btn ${!selectedItem ? "btn-disabled" : ""}`,
19675
+ className: `control-btn delete-btn ${!hasSelection ? "btn-disabled" : ""}`,
18100
19676
  children: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { className: "icon-md" })
18101
19677
  }
18102
19678
  ),
@@ -18121,6 +19697,25 @@ const PlayerControls = ({
18121
19697
  )
18122
19698
  ] }),
18123
19699
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "playback-controls", children: [
19700
+ onFollowPlayheadToggle && /* @__PURE__ */ jsxRuntime.jsx(
19701
+ "button",
19702
+ {
19703
+ onClick: onFollowPlayheadToggle,
19704
+ title: followPlayheadEnabled ? "Follow playhead on (click to disable)" : "Follow playhead off (click to enable)",
19705
+ className: `control-btn ${followPlayheadEnabled ? "follow-btn-active" : ""}`,
19706
+ children: /* @__PURE__ */ jsxRuntime.jsx(Crosshair, { className: "icon-md" })
19707
+ }
19708
+ ),
19709
+ /* @__PURE__ */ jsxRuntime.jsx(
19710
+ "button",
19711
+ {
19712
+ onClick: handleSeekToStart,
19713
+ disabled: playerState === livePlayer.PLAYER_STATE.REFRESH,
19714
+ title: "Jump to start",
19715
+ className: "control-btn",
19716
+ children: /* @__PURE__ */ jsxRuntime.jsx(SkipBack, { className: "icon-md" })
19717
+ }
19718
+ ),
18124
19719
  /* @__PURE__ */ jsxRuntime.jsx(
18125
19720
  "button",
18126
19721
  {
@@ -18131,6 +19726,16 @@ const PlayerControls = ({
18131
19726
  children: playerState === livePlayer.PLAYER_STATE.PLAYING ? /* @__PURE__ */ jsxRuntime.jsx(Pause, { className: "icon-lg" }) : playerState === livePlayer.PLAYER_STATE.REFRESH ? /* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { className: "icon-lg animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(Play, { className: "icon-lg" })
18132
19727
  }
18133
19728
  ),
19729
+ /* @__PURE__ */ jsxRuntime.jsx(
19730
+ "button",
19731
+ {
19732
+ onClick: handleSeekToEnd,
19733
+ disabled: playerState === livePlayer.PLAYER_STATE.REFRESH,
19734
+ title: "Jump to end",
19735
+ className: "control-btn",
19736
+ children: /* @__PURE__ */ jsxRuntime.jsx(SkipForward, { className: "icon-md" })
19737
+ }
19738
+ ),
18134
19739
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "time-display", children: [
18135
19740
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "current-time", children: formatTime(currentTime) }),
18136
19741
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "time-separator", children: "/" }),
@@ -18192,12 +19797,17 @@ const usePlayerControl = () => {
18192
19797
  };
18193
19798
  };
18194
19799
  const useTimelineControl = () => {
18195
- const { editor, setSelectedItem } = timeline.useTimelineContext();
19800
+ const { editor, setSelectedItem, selectedIds } = timeline.useTimelineContext();
18196
19801
  const deleteItem = (item) => {
18197
- if (item instanceof timeline.Track) {
18198
- editor.removeTrack(item);
18199
- } else if (item instanceof timeline.TrackElement) {
18200
- editor.removeElement(item);
19802
+ var _a;
19803
+ const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
19804
+ const toDelete = item !== void 0 ? [item] : timeline.resolveIds(selectedIds, tracks);
19805
+ for (const el of toDelete) {
19806
+ if (el instanceof timeline.Track) {
19807
+ editor.removeTrack(el);
19808
+ } else if (el instanceof timeline.TrackElement) {
19809
+ editor.removeElement(el);
19810
+ }
18201
19811
  }
18202
19812
  setSelectedItem(null);
18203
19813
  };
@@ -18217,32 +19827,97 @@ const useTimelineControl = () => {
18217
19827
  handleRedo
18218
19828
  };
18219
19829
  };
19830
+ function shouldIgnoreKeydown() {
19831
+ const active = document.activeElement;
19832
+ if (!active) return false;
19833
+ const tag = active.tagName.toLowerCase();
19834
+ if (tag === "input" || tag === "textarea") return true;
19835
+ if (active.isContentEditable) return true;
19836
+ return false;
19837
+ }
19838
+ function useCanvasKeyboard({
19839
+ onDelete,
19840
+ onUndo,
19841
+ onRedo,
19842
+ enabled = true
19843
+ }) {
19844
+ React.useEffect(() => {
19845
+ if (!enabled) return;
19846
+ const handleKeyDown = (e3) => {
19847
+ if (shouldIgnoreKeydown()) return;
19848
+ const key = e3.key.toLowerCase();
19849
+ const hasPrimaryModifier = e3.metaKey || e3.ctrlKey;
19850
+ if (hasPrimaryModifier) {
19851
+ if (key === "z" && !e3.shiftKey) {
19852
+ e3.preventDefault();
19853
+ onUndo == null ? void 0 : onUndo();
19854
+ return;
19855
+ }
19856
+ if (key === "y" || key === "z" && e3.shiftKey) {
19857
+ e3.preventDefault();
19858
+ onRedo == null ? void 0 : onRedo();
19859
+ return;
19860
+ }
19861
+ }
19862
+ if (!hasPrimaryModifier && (key === "delete" || key === "backspace")) {
19863
+ e3.preventDefault();
19864
+ onDelete == null ? void 0 : onDelete();
19865
+ }
19866
+ };
19867
+ document.addEventListener("keydown", handleKeyDown);
19868
+ return () => document.removeEventListener("keydown", handleKeyDown);
19869
+ }, [enabled, onDelete]);
19870
+ }
18220
19871
  const ControlManager = ({
18221
19872
  trackZoom,
18222
19873
  setTrackZoom,
18223
- zoomConfig
19874
+ zoomConfig,
19875
+ fps
18224
19876
  }) => {
18225
- const { currentTime, playerState } = livePlayer.useLivePlayerContext();
19877
+ const { currentTime, playerState, setSeekTime, setCurrentTime } = livePlayer.useLivePlayerContext();
18226
19878
  const { togglePlayback } = usePlayerControl();
18227
- const { canRedo, canUndo, totalDuration, selectedItem } = timeline.useTimelineContext();
19879
+ const {
19880
+ canRedo,
19881
+ canUndo,
19882
+ totalDuration,
19883
+ selectedItem,
19884
+ selectedIds,
19885
+ followPlayheadEnabled,
19886
+ setFollowPlayheadEnabled
19887
+ } = timeline.useTimelineContext();
18228
19888
  const { deleteItem, splitElement, handleUndo, handleRedo } = useTimelineControl();
19889
+ useCanvasKeyboard({
19890
+ onDelete: () => deleteItem(),
19891
+ onUndo: () => handleUndo(),
19892
+ onRedo: () => handleRedo()
19893
+ });
19894
+ const handleSeek = (time2) => {
19895
+ const clamped = Math.max(0, Math.min(totalDuration, time2));
19896
+ setCurrentTime(clamped);
19897
+ setSeekTime(clamped);
19898
+ };
18229
19899
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-editor-timeline-controls", children: /* @__PURE__ */ jsxRuntime.jsx(
18230
19900
  PlayerControls,
18231
19901
  {
18232
19902
  selectedItem,
19903
+ selectedIds,
18233
19904
  duration: totalDuration,
18234
19905
  currentTime,
18235
19906
  playerState,
18236
19907
  togglePlayback,
18237
19908
  canUndo,
18238
19909
  canRedo,
18239
- onDelete: deleteItem,
19910
+ onDelete: () => deleteItem(),
18240
19911
  onSplit: splitElement,
18241
19912
  onUndo: handleUndo,
18242
19913
  onRedo: handleRedo,
18243
19914
  zoomLevel: trackZoom,
18244
19915
  setZoomLevel: setTrackZoom,
18245
- zoomConfig
19916
+ zoomConfig,
19917
+ fps: fps ?? DEFAULT_FPS,
19918
+ onSeek: handleSeek,
19919
+ followPlayheadEnabled,
19920
+ onFollowPlayheadToggle: () => setFollowPlayheadEnabled(!followPlayheadEnabled)
18246
19921
  }
18247
19922
  ) });
18248
19923
  };
@@ -18269,7 +19944,8 @@ const VideoEditor = ({
18269
19944
  {
18270
19945
  videoProps: editorConfig.videoProps,
18271
19946
  playerProps: editorConfig.playerProps,
18272
- canvasMode: editorConfig.canvasMode ?? true
19947
+ canvasMode: editorConfig.canvasMode ?? true,
19948
+ canvasConfig: editorConfig.canvasConfig
18273
19949
  }
18274
19950
  ),
18275
19951
  [editorConfig]
@@ -18287,7 +19963,8 @@ const VideoEditor = ({
18287
19963
  {
18288
19964
  trackZoom,
18289
19965
  setTrackZoom,
18290
- zoomConfig
19966
+ zoomConfig,
19967
+ fps: editorConfig.fps
18291
19968
  }
18292
19969
  ) : null,
18293
19970
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -18691,6 +20368,7 @@ exports.AVAILABLE_TEXT_FONTS = AVAILABLE_TEXT_FONTS;
18691
20368
  exports.BaseMediaManager = BaseMediaManager;
18692
20369
  exports.BrowserMediaManager = BrowserMediaManager;
18693
20370
  exports.DEFAULT_ELEMENT_COLORS = DEFAULT_ELEMENT_COLORS;
20371
+ exports.DEFAULT_FPS = DEFAULT_FPS;
18694
20372
  exports.DEFAULT_TIMELINE_TICK_CONFIGS = DEFAULT_TIMELINE_TICK_CONFIGS;
18695
20373
  exports.DEFAULT_TIMELINE_ZOOM = DEFAULT_TIMELINE_ZOOM;
18696
20374
  exports.DEFAULT_TIMELINE_ZOOM_CONFIG = DEFAULT_TIMELINE_ZOOM_CONFIG;
@@ -18698,7 +20376,9 @@ exports.DRAG_TYPE = DRAG_TYPE;
18698
20376
  exports.INITIAL_TIMELINE_DATA = INITIAL_TIMELINE_DATA;
18699
20377
  exports.MIN_DURATION = MIN_DURATION;
18700
20378
  exports.PlayerControls = PlayerControls;
20379
+ exports.SNAP_THRESHOLD_PX = SNAP_THRESHOLD_PX;
18701
20380
  exports.TEXT_EFFECTS = TEXT_EFFECTS;
20381
+ exports.TIMELINE_DROP_MEDIA_TYPE = TIMELINE_DROP_MEDIA_TYPE;
18702
20382
  exports.TimelineManager = TimelineManager;
18703
20383
  exports.animationGifs = animationGifs;
18704
20384
  exports.default = VideoEditor;