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