hyperframes 0.7.44 → 0.7.46

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.
@@ -1372,10 +1372,6 @@ var usePlayerStore = create((set, get) => ({
1372
1372
  setMotionPathCreateAvailable: (available) => set({ motionPathCreateAvailable: available }),
1373
1373
  autoKeyframeEnabled: true,
1374
1374
  setAutoKeyframeEnabled: (enabled) => set({ autoKeyframeEnabled: enabled }),
1375
- cropMode: false,
1376
- setCropMode: (active) => set({ cropMode: active }),
1377
- cropAvailable: false,
1378
- setCropAvailable: (available) => set(available ? { cropAvailable: true } : { cropAvailable: false, cropMode: false }),
1379
1375
  selectedElementIds: /* @__PURE__ */ new Set(),
1380
1376
  toggleSelectedElementId: (id) => set((s) => {
1381
1377
  const next = new Set(s.selectedElementIds);
@@ -1758,10 +1754,8 @@ var SHORTCUT_SECTIONS = [
1758
1754
  {
1759
1755
  title: "Crop",
1760
1756
  hints: [
1761
- { key: "DblClick", label: "Crop selected element" },
1762
- { key: "Drag edge", label: "Adjust crop side" },
1763
- { key: "Drag frame", label: "Move crop window" },
1764
- { key: "Esc", label: "Exit crop (or click outside)" }
1757
+ { key: "Drag edge", label: "Crop a side" },
1758
+ { key: "Drag center", label: "Reposition the crop" }
1765
1759
  ]
1766
1760
  },
1767
1761
  {
@@ -16660,9 +16654,7 @@ function StyleSections({
16660
16654
  assets,
16661
16655
  onSetStyle,
16662
16656
  onImportAssets,
16663
- gsapBorderRadius,
16664
- cropMode = false,
16665
- onCropModeChange
16657
+ gsapBorderRadius
16666
16658
  }) {
16667
16659
  const styleEditingDisabled = !element.capabilities.canEditStyles;
16668
16660
  const isFlex = styles.display === "flex" || styles.display === "inline-flex";
@@ -17006,21 +16998,7 @@ function StyleSections({
17006
16998
  onCommit: (next) => commitClipInsetSide("left", next)
17007
16999
  }
17008
17000
  )
17009
- ] }) }),
17010
- /* @__PURE__ */ jsxs34(
17011
- "button",
17012
- {
17013
- type: "button",
17014
- disabled: styleEditingDisabled || !onCropModeChange,
17015
- "aria-pressed": cropMode,
17016
- onClick: () => onCropModeChange?.(!cropMode),
17017
- className: `inline-flex h-8 w-fit items-center gap-1.5 rounded-md border px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${cropMode ? "border-studio-accent/60 bg-studio-accent/15 text-studio-accent" : "border-panel-border bg-panel-input text-panel-text-2 hover:text-white"}`,
17018
- children: [
17019
- /* @__PURE__ */ jsx41(Scissors, { size: 13 }),
17020
- "Crop"
17021
- ]
17022
- }
17023
- )
17001
+ ] }) })
17024
17002
  ] }) }),
17025
17003
  /* @__PURE__ */ jsx41(Section, { title: "Transparency", icon: /* @__PURE__ */ jsx41(Eye2, { size: 15 }), defaultCollapsed: true, children: /* @__PURE__ */ jsxs34("div", { className: "space-y-4", children: [
17026
17004
  /* @__PURE__ */ jsx41(
@@ -19833,9 +19811,7 @@ var PropertyPanel = memo22(function PropertyPanel2({
19833
19811
  onSeekToTime,
19834
19812
  recordingState,
19835
19813
  recordingDuration,
19836
- onToggleRecording,
19837
- cropMode,
19838
- onCropModeChange
19814
+ onToggleRecording
19839
19815
  }) {
19840
19816
  const styles = element?.computedStyles ?? EMPTY_STYLES;
19841
19817
  const { showToast } = useStudioShellContext();
@@ -20267,9 +20243,7 @@ var PropertyPanel = memo22(function PropertyPanel2({
20267
20243
  assets,
20268
20244
  onSetStyle,
20269
20245
  onImportAssets,
20270
- gsapBorderRadius,
20271
- cropMode,
20272
- onCropModeChange
20246
+ gsapBorderRadius
20273
20247
  }
20274
20248
  )
20275
20249
  ] })
@@ -21082,7 +21056,7 @@ var FileTree = memo24(function FileTree2({
21082
21056
  });
21083
21057
 
21084
21058
  // src/App.tsx
21085
- import { useState as useState97, useCallback as useCallback114, useRef as useRef100, useMemo as useMemo39, useEffect as useEffect74, useLayoutEffect as useLayoutEffect4 } from "react";
21059
+ import { useState as useState98, useCallback as useCallback114, useRef as useRef100, useMemo as useMemo38, useEffect as useEffect73, useLayoutEffect as useLayoutEffect4 } from "react";
21086
21060
 
21087
21061
  // src/components/renders/useRenderQueue.ts
21088
21062
  import { useState as useState38, useEffect as useEffect28, useCallback as useCallback34, useRef as useRef39, useMemo as useMemo18 } from "react";
@@ -36227,123 +36201,8 @@ function useGestureCommit({
36227
36201
  return { gestureState, gestureRecording, handleToggleRecording };
36228
36202
  }
36229
36203
 
36230
- // src/hooks/useCropMode.ts
36231
- import { useEffect as useEffect50, useMemo as useMemo28, useReducer } from "react";
36232
-
36233
- // src/components/editor/domEditOverlayCrop.ts
36234
- function cropRectFromInsets(rect, insets, scaleX, scaleY) {
36235
- const sx = scaleX > 0 ? scaleX : 1;
36236
- const sy = scaleY > 0 ? scaleY : 1;
36237
- const left = rect.left + insets.left * sx;
36238
- const top = rect.top + insets.top * sy;
36239
- return {
36240
- left,
36241
- top,
36242
- width: Math.max(0, rect.width - (insets.left + insets.right) * sx),
36243
- height: Math.max(0, rect.height - (insets.top + insets.bottom) * sy)
36244
- };
36245
- }
36246
- function readElementCropInsets(element) {
36247
- const inline = element.style.getPropertyValue("clip-path").trim();
36248
- const value = inline || element.ownerDocument.defaultView?.getComputedStyle(element).clipPath.trim() || "";
36249
- const parsed = parseInsetClipPathSides(value === "none" ? "" : value);
36250
- return parsed ?? { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
36251
- }
36252
- function clampInset(value, max) {
36253
- if (!Number.isFinite(value)) return 0;
36254
- return Math.min(Math.max(0, value), Math.max(0, max));
36255
- }
36256
- function resolveCropInsetFromEdgeDrag(input) {
36257
- const scaleX = input.scaleX > 0 ? input.scaleX : 1;
36258
- const scaleY = input.scaleY > 0 ? input.scaleY : 1;
36259
- const next = { ...input.startInsets };
36260
- if (input.edge === "left") {
36261
- next.left = clampInset(
36262
- input.startInsets.left + input.deltaX / scaleX,
36263
- input.width - next.right
36264
- );
36265
- } else if (input.edge === "right") {
36266
- next.right = clampInset(
36267
- input.startInsets.right - input.deltaX / scaleX,
36268
- input.width - next.left
36269
- );
36270
- } else if (input.edge === "top") {
36271
- next.top = clampInset(
36272
- input.startInsets.top + input.deltaY / scaleY,
36273
- input.height - next.bottom
36274
- );
36275
- } else {
36276
- next.bottom = clampInset(
36277
- input.startInsets.bottom - input.deltaY / scaleY,
36278
- input.height - next.top
36279
- );
36280
- }
36281
- return next;
36282
- }
36283
- function resolveCropInsetFromMoveDrag(input) {
36284
- const sx = input.scaleX > 0 ? input.scaleX : 1;
36285
- const sy = input.scaleY > 0 ? input.scaleY : 1;
36286
- const totalX = input.startInsets.left + input.startInsets.right;
36287
- const totalY = input.startInsets.top + input.startInsets.bottom;
36288
- const left = Math.min(Math.max(0, input.startInsets.left + input.deltaX / sx), totalX);
36289
- const top = Math.min(Math.max(0, input.startInsets.top + input.deltaY / sy), totalY);
36290
- return { left, right: totalX - left, top, bottom: totalY - top };
36291
- }
36292
- function hugRectForElement(rect, element) {
36293
- const insets = readElementCropInsets(element);
36294
- if (insets.top <= 0 && insets.right <= 0 && insets.bottom <= 0 && insets.left <= 0) return rect;
36295
- return cropRectFromInsets(rect, insets, rect.editScaleX, rect.editScaleY);
36296
- }
36297
-
36298
- // src/hooks/useCropMode.ts
36299
- function useCropModeProps() {
36300
- const cropMode = usePlayerStore((s) => s.cropMode);
36301
- const setCropMode = usePlayerStore((s) => s.setCropMode);
36302
- return useMemo28(
36303
- () => ({
36304
- cropMode,
36305
- onCropModeChange: setCropMode
36306
- }),
36307
- [cropMode, setCropMode]
36308
- );
36309
- }
36310
- function useCropOverlay(params) {
36311
- const { selection, groupCount, cropMode, onCropModeChange, overlayRect } = params;
36312
- useEffect50(() => {
36313
- if (!cropMode || !onCropModeChange) return;
36314
- const handleKeyDown = (event) => {
36315
- if (event.key === "Escape") onCropModeChange(false);
36316
- };
36317
- window.addEventListener("keydown", handleKeyDown);
36318
- return () => window.removeEventListener("keydown", handleKeyDown);
36319
- }, [cropMode, onCropModeChange]);
36320
- const setCropAvailable = usePlayerStore((s) => s.setCropAvailable);
36321
- const cropAvailable = Boolean(selection && groupCount <= 1 && selection.capabilities.canCrop);
36322
- useEffect50(() => {
36323
- setCropAvailable(cropAvailable);
36324
- return () => setCropAvailable(false);
36325
- }, [cropAvailable, setCropAvailable]);
36326
- const [, bumpAfterExit] = useReducer((x) => x + 1, 0);
36327
- useEffect50(() => {
36328
- if (!cropMode) bumpAfterExit();
36329
- }, [cropMode]);
36330
- const cropInsets = selection ? readElementCropInsets(selection.element) : null;
36331
- const hasCropInsets = Boolean(
36332
- cropInsets && (cropInsets.top > 0 || cropInsets.right > 0 || cropInsets.bottom > 0 || cropInsets.left > 0)
36333
- );
36334
- const sx = overlayRect && overlayRect.editScaleX > 0 ? overlayRect.editScaleX : 1;
36335
- const sy = overlayRect && overlayRect.editScaleY > 0 ? overlayRect.editScaleY : 1;
36336
- const cropOutlineInsetPx = cropInsets && hasCropInsets && !cropMode ? {
36337
- top: cropInsets.top * sy,
36338
- right: cropInsets.right * sx,
36339
- bottom: cropInsets.bottom * sy,
36340
- left: cropInsets.left * sx
36341
- } : void 0;
36342
- return { hasCropInsets, cropOutlineInsetPx };
36343
- }
36344
-
36345
36204
  // src/components/editor/GestureTrailOverlay.tsx
36346
- import { memo as memo26, useMemo as useMemo29 } from "react";
36205
+ import { memo as memo26, useMemo as useMemo28 } from "react";
36347
36206
  import { Fragment as Fragment19, jsx as jsx71, jsxs as jsxs60 } from "react/jsx-runtime";
36348
36207
  var GestureTrailOverlay = memo26(function GestureTrailOverlay2({
36349
36208
  samples,
@@ -36355,7 +36214,7 @@ var GestureTrailOverlay = memo26(function GestureTrailOverlay2({
36355
36214
  mode,
36356
36215
  accentColor = "#3CE6AC"
36357
36216
  }) {
36358
- const trailPoints = useMemo29(() => {
36217
+ const trailPoints = useMemo28(() => {
36359
36218
  if (!canvasRect) return "";
36360
36219
  if (trail && trail.length > 1) {
36361
36220
  return trail.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -36363,7 +36222,7 @@ var GestureTrailOverlay = memo26(function GestureTrailOverlay2({
36363
36222
  if (samples.length === 0) return "";
36364
36223
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
36365
36224
  }, [samples, trail, sampleCount, canvasRect?.left, canvasRect?.top]);
36366
- const simplifiedPath = useMemo29(() => {
36225
+ const simplifiedPath = useMemo28(() => {
36367
36226
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
36368
36227
  const pts = [];
36369
36228
  for (const [pct, props] of simplifiedPoints) {
@@ -36375,7 +36234,7 @@ var GestureTrailOverlay = memo26(function GestureTrailOverlay2({
36375
36234
  if (pts.length === 0) return "";
36376
36235
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
36377
36236
  }, [simplifiedPoints]);
36378
- const diamondPositions = useMemo29(() => {
36237
+ const diamondPositions = useMemo28(() => {
36379
36238
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
36380
36239
  const pts = [];
36381
36240
  for (const [pct, props] of simplifiedPoints) {
@@ -36468,7 +36327,7 @@ import {
36468
36327
  } from "react";
36469
36328
 
36470
36329
  // src/components/sidebar/CompositionsTab.tsx
36471
- import { memo as memo27, useCallback as useCallback86, useEffect as useEffect51, useRef as useRef76, useState as useState62 } from "react";
36330
+ import { memo as memo27, useCallback as useCallback86, useEffect as useEffect50, useRef as useRef76, useState as useState62 } from "react";
36472
36331
  import { jsx as jsx72, jsxs as jsxs61 } from "react/jsx-runtime";
36473
36332
  var DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
36474
36333
  var CARD_W = 80;
@@ -36570,10 +36429,10 @@ function CompCard({
36570
36429
  });
36571
36430
  const thumbnailOffsetX = (CARD_W - stageSize.width * previewScale) / 2;
36572
36431
  const thumbnailOffsetY = (CARD_H - stageSize.height * previewScale) / 2;
36573
- useEffect51(() => {
36432
+ useEffect50(() => {
36574
36433
  requestIframePlaybackSync(hovered);
36575
36434
  }, [hovered, requestIframePlaybackSync]);
36576
- useEffect51(() => {
36435
+ useEffect50(() => {
36577
36436
  return () => {
36578
36437
  if (hoverTimer.current) clearTimeout(hoverTimer.current);
36579
36438
  if (syncTimer.current) clearTimeout(syncTimer.current);
@@ -36697,10 +36556,10 @@ var CompositionsTab = memo27(function CompositionsTab2({
36697
36556
  });
36698
36557
 
36699
36558
  // src/components/sidebar/AssetsTab.tsx
36700
- import { memo as memo28, useState as useState66, useCallback as useCallback88, useRef as useRef78, useMemo as useMemo31, useEffect as useEffect55 } from "react";
36559
+ import { memo as memo28, useState as useState66, useCallback as useCallback88, useRef as useRef78, useMemo as useMemo30, useEffect as useEffect54 } from "react";
36701
36560
 
36702
36561
  // src/components/ui/VideoFrameThumbnail.tsx
36703
- import { useState as useState63, useEffect as useEffect52 } from "react";
36562
+ import { useState as useState63, useEffect as useEffect51 } from "react";
36704
36563
  import { jsx as jsx73 } from "react/jsx-runtime";
36705
36564
  function VideoFrameThumbnail({
36706
36565
  src,
@@ -36708,7 +36567,7 @@ function VideoFrameThumbnail({
36708
36567
  }) {
36709
36568
  const [frame, setFrame] = useState63(null);
36710
36569
  const [failed2, setFailed] = useState63(false);
36711
- useEffect52(() => {
36570
+ useEffect51(() => {
36712
36571
  setFailed(false);
36713
36572
  const video = document.createElement("video");
36714
36573
  video.crossOrigin = "anonymous";
@@ -36850,7 +36709,7 @@ var CATEGORY_LABELS = {
36850
36709
  var FILTER_ORDER = ["audio", "images", "video", "fonts"];
36851
36710
 
36852
36711
  // src/components/sidebar/AudioRow.tsx
36853
- import { useState as useState64, useRef as useRef77, useEffect as useEffect53, useCallback as useCallback87 } from "react";
36712
+ import { useState as useState64, useRef as useRef77, useEffect as useEffect52, useCallback as useCallback87 } from "react";
36854
36713
  import { Fragment as Fragment20, jsx as jsx75, jsxs as jsxs63 } from "react/jsx-runtime";
36855
36714
  function AudioRow({
36856
36715
  projectId,
@@ -36873,14 +36732,14 @@ function AudioRow({
36873
36732
  const name = basename(asset);
36874
36733
  const subtype = getAudioSubtype(asset);
36875
36734
  const serveUrl = `/api/projects/${projectId}/preview/${asset}`;
36876
- useEffect53(() => {
36735
+ useEffect52(() => {
36877
36736
  return () => {
36878
36737
  cancelAnimationFrame(animRef.current);
36879
36738
  audioRef.current?.pause();
36880
36739
  actxRef.current?.close();
36881
36740
  };
36882
36741
  }, []);
36883
- useEffect53(() => {
36742
+ useEffect52(() => {
36884
36743
  if (playing) {
36885
36744
  const barCount = 24;
36886
36745
  const loop = () => {
@@ -37016,7 +36875,7 @@ function AudioRow({
37016
36875
  }
37017
36876
 
37018
36877
  // src/components/sidebar/GlobalAssetsView.tsx
37019
- import { useEffect as useEffect54, useMemo as useMemo30, useState as useState65 } from "react";
36878
+ import { useEffect as useEffect53, useMemo as useMemo29, useState as useState65 } from "react";
37020
36879
  import { jsx as jsx76, jsxs as jsxs64 } from "react/jsx-runtime";
37021
36880
  function globalAssetRows(records, query = "") {
37022
36881
  const q = query.trim().toLowerCase();
@@ -37032,7 +36891,7 @@ function globalAssetRows(records, query = "") {
37032
36891
  }
37033
36892
  function GlobalAssetsView({ searchQuery }) {
37034
36893
  const [records, setRecords] = useState65(null);
37035
- useEffect54(() => {
36894
+ useEffect53(() => {
37036
36895
  let cancelled = false;
37037
36896
  fetch("/api/assets/global").then((r) => r.ok ? r.json() : { assets: [] }).then((d) => {
37038
36897
  if (!cancelled) setRecords(Array.isArray(d.assets) ? d.assets : []);
@@ -37043,7 +36902,7 @@ function GlobalAssetsView({ searchQuery }) {
37043
36902
  cancelled = true;
37044
36903
  };
37045
36904
  }, []);
37046
- const rows = useMemo30(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
36905
+ const rows = useMemo29(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
37047
36906
  if (records === null) {
37048
36907
  return /* @__PURE__ */ jsx76("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
37049
36908
  }
@@ -37233,7 +37092,7 @@ var AssetsTab = memo28(function AssetsTab2({
37233
37092
  const [manifest, setManifest] = useState66(/* @__PURE__ */ new Map());
37234
37093
  const manifest404Ref = useRef78(/* @__PURE__ */ new Set());
37235
37094
  const assetsKey = assets.join("|");
37236
- useEffect55(() => {
37095
+ useEffect54(() => {
37237
37096
  if (manifest404Ref.current.has(projectId)) return;
37238
37097
  let cancelled = false;
37239
37098
  fetch(`/api/projects/${projectId}/preview/.media/manifest.jsonl`).then((r) => {
@@ -37276,8 +37135,8 @@ var AssetsTab = memo28(function AssetsTab2({
37276
37135
  }
37277
37136
  }, []);
37278
37137
  const elements = usePlayerStore((s) => s.elements);
37279
- const usedPaths = useMemo31(() => deriveUsedPaths(elements), [elements]);
37280
- const mediaAssets = useMemo31(() => {
37138
+ const usedPaths = useMemo30(() => deriveUsedPaths(elements), [elements]);
37139
+ const mediaAssets = useMemo30(() => {
37281
37140
  const media = assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a));
37282
37141
  const all = filterByUsage(media, usedPaths, usageFilter);
37283
37142
  if (!searchQuery) return all;
@@ -37288,7 +37147,7 @@ var AssetsTab = memo28(function AssetsTab2({
37288
37147
  return rec?.description?.toLowerCase().includes(q);
37289
37148
  });
37290
37149
  }, [assets, searchQuery, manifest, usageFilter, usedPaths]);
37291
- const categorized = useMemo31(() => {
37150
+ const categorized = useMemo30(() => {
37292
37151
  const groups = { audio: [], images: [], video: [], fonts: [] };
37293
37152
  for (const a of mediaAssets) {
37294
37153
  const cat = getCategory(a);
@@ -37303,12 +37162,12 @@ var AssetsTab = memo28(function AssetsTab2({
37303
37162
  }
37304
37163
  return groups;
37305
37164
  }, [mediaAssets, usedPaths]);
37306
- const counts = useMemo31(() => {
37165
+ const counts = useMemo30(() => {
37307
37166
  const c = { all: mediaAssets.length };
37308
37167
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
37309
37168
  return c;
37310
37169
  }, [mediaAssets, categorized]);
37311
- const usageCounts = useMemo31(
37170
+ const usageCounts = useMemo30(
37312
37171
  () => countUsage(
37313
37172
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
37314
37173
  usedPaths
@@ -37550,11 +37409,11 @@ var AssetsTab = memo28(function AssetsTab2({
37550
37409
  });
37551
37410
 
37552
37411
  // src/components/sidebar/BlocksTab.tsx
37553
- import { memo as memo29, useState as useState68, useCallback as useCallback89, useRef as useRef79, useEffect as useEffect57 } from "react";
37412
+ import { memo as memo29, useState as useState68, useCallback as useCallback89, useRef as useRef79, useEffect as useEffect56 } from "react";
37554
37413
  import { createPortal as createPortal5 } from "react-dom";
37555
37414
 
37556
37415
  // src/hooks/useBlockCatalog.ts
37557
- import { useState as useState67, useEffect as useEffect56, useMemo as useMemo32 } from "react";
37416
+ import { useState as useState67, useEffect as useEffect55, useMemo as useMemo31 } from "react";
37558
37417
 
37559
37418
  // src/utils/blockCategories.ts
37560
37419
  import {
@@ -37583,7 +37442,7 @@ function useBlockCatalog() {
37583
37442
  const [error, setError] = useState67(null);
37584
37443
  const [search, setSearch] = useState67("");
37585
37444
  const [category, setCategory] = useState67(null);
37586
- useEffect56(() => {
37445
+ useEffect55(() => {
37587
37446
  let cancelled = false;
37588
37447
  (async () => {
37589
37448
  try {
@@ -37608,7 +37467,7 @@ function useBlockCatalog() {
37608
37467
  cancelled = true;
37609
37468
  };
37610
37469
  }, []);
37611
- const filteredBlocks = useMemo32(() => {
37470
+ const filteredBlocks = useMemo31(() => {
37612
37471
  let result = blocks;
37613
37472
  if (category) {
37614
37473
  result = result.filter((b) => b.category === category);
@@ -37861,7 +37720,7 @@ function BlockCard({
37861
37720
  setHovered(false);
37862
37721
  onPreview?.(null);
37863
37722
  }, [onPreview]);
37864
- useEffect57(() => {
37723
+ useEffect56(() => {
37865
37724
  return () => {
37866
37725
  if (hoverTimer.current) clearTimeout(hoverTimer.current);
37867
37726
  };
@@ -38016,7 +37875,7 @@ function PromptPreviewModal({
38016
37875
  const [value, setValue] = useState68(prompt);
38017
37876
  const [copied, setCopied] = useState68(false);
38018
37877
  const textareaRef = useRef79(null);
38019
- useEffect57(() => {
37878
+ useEffect56(() => {
38020
37879
  requestAnimationFrame(() => textareaRef.current?.focus());
38021
37880
  }, []);
38022
37881
  const handleCopy = useCallback89(() => {
@@ -38604,7 +38463,7 @@ function StudioLeftSidebar({
38604
38463
  }
38605
38464
 
38606
38465
  // src/components/StudioPreviewArea.tsx
38607
- import { useState as useState81, useMemo as useMemo35, useCallback as useCallback98 } from "react";
38466
+ import { useState as useState81, useMemo as useMemo34, useCallback as useCallback98 } from "react";
38608
38467
 
38609
38468
  // src/captions/components/CaptionOverlay.tsx
38610
38469
  import { memo as memo31, useState as useState71, useCallback as useCallback92, useRef as useRef81 } from "react";
@@ -39231,7 +39090,7 @@ var CaptionTimeline = memo32(function CaptionTimeline2({
39231
39090
  });
39232
39091
 
39233
39092
  // src/components/editor/DomEditOverlay.tsx
39234
- import { memo as memo35, useEffect as useEffect59, useMemo as useMemo33, useRef as useRef87, useState as useState76 } from "react";
39093
+ import { memo as memo35, useEffect as useEffect58, useMemo as useMemo32, useRef as useRef87, useState as useState76 } from "react";
39235
39094
 
39236
39095
  // src/components/editor/marqueeCommit.ts
39237
39096
  import { useCallback as useCallback94, useRef as useRef83, useState as useState72 } from "react";
@@ -39241,6 +39100,71 @@ function rectsOverlap(a, b) {
39241
39100
  return a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
39242
39101
  }
39243
39102
 
39103
+ // src/components/editor/domEditOverlayCrop.ts
39104
+ function cropRectFromInsets(rect, insets, scaleX, scaleY) {
39105
+ const sx = scaleX > 0 ? scaleX : 1;
39106
+ const sy = scaleY > 0 ? scaleY : 1;
39107
+ const left = rect.left + insets.left * sx;
39108
+ const top = rect.top + insets.top * sy;
39109
+ return {
39110
+ left,
39111
+ top,
39112
+ width: Math.max(0, rect.width - (insets.left + insets.right) * sx),
39113
+ height: Math.max(0, rect.height - (insets.top + insets.bottom) * sy)
39114
+ };
39115
+ }
39116
+ function readElementCropInsets(element) {
39117
+ const inline = element.style.getPropertyValue("clip-path").trim();
39118
+ const value = inline || element.ownerDocument.defaultView?.getComputedStyle(element).clipPath.trim() || "";
39119
+ const parsed = parseInsetClipPathSides(value === "none" ? "" : value);
39120
+ return parsed ?? { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
39121
+ }
39122
+ function clampInset(value, max) {
39123
+ if (!Number.isFinite(value)) return 0;
39124
+ return Math.min(Math.max(0, value), Math.max(0, max));
39125
+ }
39126
+ function resolveCropInsetFromEdgeDrag(input) {
39127
+ const scaleX = input.scaleX > 0 ? input.scaleX : 1;
39128
+ const scaleY = input.scaleY > 0 ? input.scaleY : 1;
39129
+ const next = { ...input.startInsets };
39130
+ if (input.edge === "left") {
39131
+ next.left = clampInset(
39132
+ input.startInsets.left + input.deltaX / scaleX,
39133
+ input.width - next.right
39134
+ );
39135
+ } else if (input.edge === "right") {
39136
+ next.right = clampInset(
39137
+ input.startInsets.right - input.deltaX / scaleX,
39138
+ input.width - next.left
39139
+ );
39140
+ } else if (input.edge === "top") {
39141
+ next.top = clampInset(
39142
+ input.startInsets.top + input.deltaY / scaleY,
39143
+ input.height - next.bottom
39144
+ );
39145
+ } else {
39146
+ next.bottom = clampInset(
39147
+ input.startInsets.bottom - input.deltaY / scaleY,
39148
+ input.height - next.top
39149
+ );
39150
+ }
39151
+ return next;
39152
+ }
39153
+ function resolveCropInsetFromMoveDrag(input) {
39154
+ const sx = input.scaleX > 0 ? input.scaleX : 1;
39155
+ const sy = input.scaleY > 0 ? input.scaleY : 1;
39156
+ const totalX = input.startInsets.left + input.startInsets.right;
39157
+ const totalY = input.startInsets.top + input.startInsets.bottom;
39158
+ const left = Math.min(Math.max(0, input.startInsets.left + input.deltaX / sx), totalX);
39159
+ const top = Math.min(Math.max(0, input.startInsets.top + input.deltaY / sy), totalY);
39160
+ return { left, right: totalX - left, top, bottom: totalY - top };
39161
+ }
39162
+ function hugRectForElement(rect, element) {
39163
+ const insets = readElementCropInsets(element);
39164
+ if (insets.top <= 0 && insets.right <= 0 && insets.bottom <= 0 && insets.left <= 0) return rect;
39165
+ return cropRectFromInsets(rect, insets, rect.editScaleX, rect.editScaleY);
39166
+ }
39167
+
39244
39168
  // src/components/editor/domEditOverlayGeometry.ts
39245
39169
  function isElementVisibleForOverlay(el) {
39246
39170
  return isElementVisibleThroughAncestors(el);
@@ -41272,13 +41196,30 @@ var GridOverlay = memo34(function GridOverlay2({
41272
41196
  });
41273
41197
 
41274
41198
  // src/components/editor/DomEditCropHandles.tsx
41275
- import { useEffect as useEffect58, useRef as useRef86, useState as useState74 } from "react";
41199
+ import { useEffect as useEffect57, useRef as useRef86, useState as useState74 } from "react";
41276
41200
  import { Fragment as Fragment27, jsx as jsx88, jsxs as jsxs74 } from "react/jsx-runtime";
41277
- function handleCenter(edge, rect) {
41278
- if (edge === "top") return { left: rect.left + rect.width / 2, top: rect.top };
41279
- if (edge === "right") return { left: rect.left + rect.width, top: rect.top + rect.height / 2 };
41280
- if (edge === "bottom") return { left: rect.left + rect.width / 2, top: rect.top + rect.height };
41281
- return { left: rect.left, top: rect.top + rect.height / 2 };
41201
+ var EDGE_HANDLE_GAP = 8;
41202
+ function edgeHandlePlacement(edge, rect) {
41203
+ const cx = rect.left + rect.width / 2;
41204
+ const cy = rect.top + rect.height / 2;
41205
+ if (edge === "top") {
41206
+ return { left: cx, top: rect.top - EDGE_HANDLE_GAP, transform: "translate(-50%, -100%)" };
41207
+ }
41208
+ if (edge === "bottom") {
41209
+ return {
41210
+ left: cx,
41211
+ top: rect.top + rect.height + EDGE_HANDLE_GAP,
41212
+ transform: "translate(-50%, 0)"
41213
+ };
41214
+ }
41215
+ if (edge === "left") {
41216
+ return { left: rect.left - EDGE_HANDLE_GAP, top: cy, transform: "translate(-100%, -50%)" };
41217
+ }
41218
+ return {
41219
+ left: rect.left + rect.width + EDGE_HANDLE_GAP,
41220
+ top: cy,
41221
+ transform: "translate(0, -50%)"
41222
+ };
41282
41223
  }
41283
41224
  var EDGES = ["top", "right", "bottom", "left"];
41284
41225
  function DomEditCropHandles({
@@ -41287,6 +41228,7 @@ function DomEditCropHandles({
41287
41228
  onStyleCommit
41288
41229
  }) {
41289
41230
  const gestureRef = useRef86(null);
41231
+ const [dragging, setDragging] = useState74(false);
41290
41232
  const [state, setState] = useState74(() => {
41291
41233
  const parsed = readElementCropInsets(selection.element);
41292
41234
  return {
@@ -41301,20 +41243,23 @@ function DomEditCropHandles({
41301
41243
  };
41302
41244
  });
41303
41245
  if (state.element !== selection.element) {
41304
- const parsed = readElementCropInsets(selection.element);
41246
+ const liveInsets = readElementCropInsets(selection.element);
41305
41247
  setState({
41306
41248
  element: selection.element,
41307
- insets: { top: parsed.top, right: parsed.right, bottom: parsed.bottom, left: parsed.left },
41308
- radius: parsed.radius
41249
+ insets: {
41250
+ top: liveInsets.top,
41251
+ right: liveInsets.right,
41252
+ bottom: liveInsets.bottom,
41253
+ left: liveInsets.left
41254
+ },
41255
+ radius: liveInsets.radius
41309
41256
  });
41310
41257
  }
41258
+ const hasCrop = state.insets.top > 0 || state.insets.right > 0 || state.insets.bottom > 0 || state.insets.left > 0;
41311
41259
  const committedRef = useRef86(null);
41312
- {
41313
- const hasCrop = state.insets.top > 0 || state.insets.right > 0 || state.insets.bottom > 0 || state.insets.left > 0;
41314
- committedRef.current = hasCrop ? buildInsetClipPathSides(state.insets, state.radius) : null;
41315
- }
41260
+ committedRef.current = hasCrop ? buildInsetClipPathSides(state.insets, state.radius) : null;
41316
41261
  const liftedRef = useRef86(false);
41317
- useEffect58(() => {
41262
+ useEffect57(() => {
41318
41263
  const el = selection.element;
41319
41264
  el.style.setProperty("clip-path", "none");
41320
41265
  liftedRef.current = true;
@@ -41342,6 +41287,7 @@ function DomEditCropHandles({
41342
41287
  startInsets: state.insets,
41343
41288
  didMove: false
41344
41289
  };
41290
+ setDragging(true);
41345
41291
  };
41346
41292
  const updateCropGesture = (event) => {
41347
41293
  const gesture = gestureRef.current;
@@ -41365,24 +41311,27 @@ function DomEditCropHandles({
41365
41311
  event.preventDefault();
41366
41312
  event.stopPropagation();
41367
41313
  gestureRef.current = null;
41314
+ setDragging(false);
41368
41315
  if (!gesture.didMove) return;
41369
41316
  const el = selection.element;
41317
+ const reLift = () => {
41318
+ if (liftedRef.current) el.style.setProperty("clip-path", "none");
41319
+ };
41370
41320
  void Promise.resolve(
41371
41321
  onStyleCommit?.("clip-path", buildInsetClipPathSides(state.insets, state.radius))
41372
- ).then(() => {
41373
- if (liftedRef.current) el.style.setProperty("clip-path", "none");
41374
- });
41322
+ ).then(reLift, reLift);
41375
41323
  };
41376
41324
  const cancelCropGesture = (event) => {
41377
41325
  const gesture = gestureRef.current;
41378
41326
  if (!gesture || gesture.pointerId !== event.pointerId) return;
41379
41327
  event.preventDefault();
41380
41328
  event.stopPropagation();
41381
- setState((prev) => ({ ...prev, insets: gesture.startInsets }));
41382
41329
  gestureRef.current = null;
41330
+ setDragging(false);
41331
+ setState((prev) => ({ ...prev, insets: gesture.startInsets }));
41383
41332
  };
41384
41333
  return /* @__PURE__ */ jsxs74(Fragment27, { children: [
41385
- /* @__PURE__ */ jsx88(
41334
+ hasCrop && /* @__PURE__ */ jsx88(
41386
41335
  "div",
41387
41336
  {
41388
41337
  className: "pointer-events-none absolute overflow-hidden",
@@ -41410,13 +41359,35 @@ function DomEditCropHandles({
41410
41359
  /* @__PURE__ */ jsx88(
41411
41360
  "div",
41412
41361
  {
41413
- "data-dom-edit-crop-frame": "true",
41414
- className: "pointer-events-auto absolute border-2 border-studio-accent shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
41362
+ "aria-hidden": "true",
41363
+ className: "pointer-events-none absolute border border-dashed border-studio-accent",
41415
41364
  style: {
41416
41365
  left: cropRect.left,
41417
41366
  top: cropRect.top,
41418
41367
  width: cropRect.width,
41419
- height: cropRect.height,
41368
+ height: cropRect.height
41369
+ },
41370
+ children: dragging && /* @__PURE__ */ jsxs74(Fragment27, { children: [
41371
+ /* @__PURE__ */ jsx88("div", { className: "absolute inset-y-0 left-1/3 w-px bg-studio-accent/40" }),
41372
+ /* @__PURE__ */ jsx88("div", { className: "absolute inset-y-0 left-2/3 w-px bg-studio-accent/40" }),
41373
+ /* @__PURE__ */ jsx88("div", { className: "absolute inset-x-0 top-1/3 h-px bg-studio-accent/40" }),
41374
+ /* @__PURE__ */ jsx88("div", { className: "absolute inset-x-0 top-2/3 h-px bg-studio-accent/40" })
41375
+ ] })
41376
+ }
41377
+ ),
41378
+ hasCrop && /* @__PURE__ */ jsx88(
41379
+ "button",
41380
+ {
41381
+ type: "button",
41382
+ "aria-label": "Reposition crop",
41383
+ "data-dom-edit-crop-handle": "true",
41384
+ className: "pointer-events-auto absolute rounded-full border-2 border-studio-accent bg-studio-accent/30 shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
41385
+ style: {
41386
+ left: cropRect.left + cropRect.width / 2,
41387
+ top: cropRect.top + cropRect.height / 2,
41388
+ width: 22,
41389
+ height: 22,
41390
+ transform: "translate(-50%, -50%)",
41420
41391
  cursor: "move",
41421
41392
  touchAction: "none"
41422
41393
  },
@@ -41427,21 +41398,21 @@ function DomEditCropHandles({
41427
41398
  }
41428
41399
  ),
41429
41400
  EDGES.map((edge) => {
41430
- const center = handleCenter(edge, cropRect);
41431
41401
  const vertical = edge === "left" || edge === "right";
41402
+ const place = edgeHandlePlacement(edge, cropRect);
41432
41403
  return /* @__PURE__ */ jsx88(
41433
41404
  "button",
41434
41405
  {
41435
41406
  type: "button",
41436
41407
  "aria-label": `Crop ${edge}`,
41437
41408
  "data-dom-edit-crop-handle": "true",
41438
- className: "pointer-events-auto absolute rounded-sm border border-studio-accent bg-studio-accent shadow-[0_0_0_2px_rgba(60,230,172,0.18)]",
41409
+ className: "pointer-events-auto absolute rounded-full bg-studio-accent shadow-[0_0_0_1px_rgba(0,0,0,0.4)]",
41439
41410
  style: {
41440
- left: center.left,
41441
- top: center.top,
41442
- width: vertical ? 10 : 28,
41443
- height: vertical ? 28 : 10,
41444
- transform: "translate(-50%, -50%)",
41411
+ left: place.left,
41412
+ top: place.top,
41413
+ width: vertical ? 5 : 26,
41414
+ height: vertical ? 26 : 5,
41415
+ transform: place.transform,
41445
41416
  cursor: vertical ? "ew-resize" : "ns-resize",
41446
41417
  touchAction: "none"
41447
41418
  },
@@ -41496,6 +41467,24 @@ function DomEditRotateHandle({
41496
41467
  );
41497
41468
  }
41498
41469
 
41470
+ // src/hooks/useCropOverlay.ts
41471
+ function useCropOverlay(params) {
41472
+ const { selection, overlayRect } = params;
41473
+ const cropInsets = selection ? readElementCropInsets(selection.element) : null;
41474
+ const hasCropInsets = Boolean(
41475
+ cropInsets && (cropInsets.top > 0 || cropInsets.right > 0 || cropInsets.bottom > 0 || cropInsets.left > 0)
41476
+ );
41477
+ const sx = overlayRect && overlayRect.editScaleX > 0 ? overlayRect.editScaleX : 1;
41478
+ const sy = overlayRect && overlayRect.editScaleY > 0 ? overlayRect.editScaleY : 1;
41479
+ const cropOutlineInsetPx = cropInsets && hasCropInsets ? {
41480
+ top: cropInsets.top * sy,
41481
+ right: cropInsets.right * sx,
41482
+ bottom: cropInsets.bottom * sy,
41483
+ left: cropInsets.left * sx
41484
+ } : void 0;
41485
+ return { hasCropInsets, cropOutlineInsetPx };
41486
+ }
41487
+
41499
41488
  // src/components/editor/domEditOverlayShape.ts
41500
41489
  function readDomEditSelectionShapeStyles(selection) {
41501
41490
  const fallback = {
@@ -41713,8 +41702,6 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41713
41702
  onBoxSizeCommit,
41714
41703
  onRotationCommit,
41715
41704
  onStyleCommit,
41716
- cropMode = false,
41717
- onCropModeChange,
41718
41705
  onMarqueeSelect
41719
41706
  }) {
41720
41707
  const overlayRef = useRef87(null);
@@ -41781,9 +41768,6 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41781
41768
  compRectRef.current = compRect;
41782
41769
  const { hasCropInsets, cropOutlineInsetPx } = useCropOverlay({
41783
41770
  selection,
41784
- groupCount: groupSelections.length,
41785
- cropMode,
41786
- onCropModeChange,
41787
41771
  overlayRect
41788
41772
  });
41789
41773
  const boxClipPath = hasCropInsets ? void 0 : selectionShapeStyles.clipPath;
@@ -41808,7 +41792,7 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41808
41792
  setRects: setOffCanvasRects
41809
41793
  })
41810
41794
  );
41811
- useEffect59(() => {
41795
+ useEffect58(() => {
41812
41796
  offCanvasDirtyRef.current = true;
41813
41797
  }, [activeCompositionPath]);
41814
41798
  const gestures = createDomEditOverlayGestureHandlers({
@@ -41844,11 +41828,11 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41844
41828
  selectionRef,
41845
41829
  gestures
41846
41830
  });
41847
- const selectionKey = useMemo33(() => {
41831
+ const selectionKey = useMemo32(() => {
41848
41832
  if (!selection) return "none";
41849
41833
  return `${selection.sourceFile}:${selection.id ?? selection.selector ?? selection.label}:${selection.selectorIndex ?? 0}`;
41850
41834
  }, [selection]);
41851
- const groupBounds = useMemo33(
41835
+ const groupBounds = useMemo32(
41852
41836
  () => resolveDomEditGroupOverlayRect(groupOverlayItems.map((item) => item.rect)),
41853
41837
  [groupOverlayItems]
41854
41838
  );
@@ -41856,11 +41840,6 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41856
41840
  const groupCanMove = hasGroupSelection && groupOverlayItems.length > 1 && groupOverlayItems.every((item) => item.selection.capabilities.canApplyManualOffset);
41857
41841
  const handleOverlayMouseDown = (event) => {
41858
41842
  if (!allowCanvasMovement) return;
41859
- if (cropMode) {
41860
- event.preventDefault();
41861
- event.stopPropagation();
41862
- return;
41863
- }
41864
41843
  if (suppressNextOverlayMouseDownRef.current) {
41865
41844
  suppressNextOverlayMouseDownRef.current = false;
41866
41845
  suppressNextBoxMouseDownRef.current = false;
@@ -41879,12 +41858,6 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41879
41858
  };
41880
41859
  const handleOverlayPointerDown = (event) => {
41881
41860
  if (!allowCanvasMovement || event.button !== 0) return;
41882
- if (cropMode) {
41883
- event.preventDefault();
41884
- event.stopPropagation();
41885
- onCropModeChange?.(false);
41886
- return;
41887
- }
41888
41861
  if (event.shiftKey) {
41889
41862
  const candidate = hoverSelectionRef.current;
41890
41863
  if (!candidate) return;
@@ -41923,14 +41896,8 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41923
41896
  }
41924
41897
  }
41925
41898
  };
41926
- const lastBoxPointerDownAtRef = useRef87(0);
41927
41899
  const handleBoxClick = (event) => {
41928
41900
  if (!allowCanvasMovement) return;
41929
- if (cropMode) {
41930
- event.preventDefault();
41931
- event.stopPropagation();
41932
- return;
41933
- }
41934
41901
  if (gestureRef.current || groupGestureRef.current) return;
41935
41902
  if (suppressNextBoxClickRef.current) {
41936
41903
  suppressNextBoxClickRef.current = false;
@@ -41956,10 +41923,10 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
41956
41923
  onPointerDownCapture: (event) => focusDomEditOverlayElement(event.currentTarget),
41957
41924
  onPointerDown: handleOverlayPointerDown,
41958
41925
  onMouseDown: handleOverlayMouseDown,
41959
- onPointerMove: cropMode ? void 0 : marquee.onPointerMove,
41926
+ onPointerMove: marquee.onPointerMove,
41960
41927
  onPointerLeave: () => onCanvasPointerLeaveRef.current(),
41961
- onPointerUp: cropMode ? void 0 : marquee.onPointerUp,
41962
- onPointerCancel: cropMode ? void 0 : marquee.onPointerCancel,
41928
+ onPointerUp: marquee.onPointerUp,
41929
+ onPointerCancel: marquee.onPointerCancel,
41963
41930
  children: [
41964
41931
  hoverSelection && hoverRect && compRect.width > 0 && /* @__PURE__ */ jsx90(
41965
41932
  "div",
@@ -42007,7 +41974,7 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
42007
41974
  )
42008
41975
  ] }),
42009
41976
  !hasGroupSelection && selection && overlayRect && compRect.width > 0 && /* @__PURE__ */ jsxs76(Fragment28, { children: [
42010
- allowCanvasMovement && !cropMode && selection.capabilities.canApplyManualRotation && /* @__PURE__ */ jsx90(
41977
+ allowCanvasMovement && selection.capabilities.canApplyManualRotation && /* @__PURE__ */ jsx90(
42011
41978
  DomEditRotateHandle,
42012
41979
  {
42013
41980
  overlayRect,
@@ -42030,25 +41997,10 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
42030
41997
  width: overlayRect.width,
42031
41998
  height: overlayRect.height,
42032
41999
  clipPath: boxClipPath,
42033
- cursor: allowCanvasMovement && !cropMode && selection.capabilities.canApplyManualOffset ? "move" : "default"
42000
+ cursor: allowCanvasMovement && selection.capabilities.canApplyManualOffset ? "move" : "default"
42034
42001
  },
42035
42002
  onPointerDown: (e) => {
42036
- if (cropMode) {
42037
- e.preventDefault();
42038
- e.stopPropagation();
42039
- return;
42040
- }
42041
42003
  if (!allowCanvasMovement || e.shiftKey) return;
42042
- const now = Date.now();
42043
- const isDoubleClick = now - lastBoxPointerDownAtRef.current < 400;
42044
- lastBoxPointerDownAtRef.current = now;
42045
- if (isDoubleClick && onCropModeChange && selection.capabilities.canCrop) {
42046
- lastBoxPointerDownAtRef.current = 0;
42047
- e.preventDefault();
42048
- e.stopPropagation();
42049
- onCropModeChange(true);
42050
- return;
42051
- }
42052
42004
  if (selection.capabilities.canApplyManualOffset) {
42053
42005
  gestures.startGesture("drag", e);
42054
42006
  return;
@@ -42078,7 +42030,7 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
42078
42030
  }
42079
42031
  }
42080
42032
  ),
42081
- allowCanvasMovement && !cropMode && selection.capabilities.canApplyManualSize && /* @__PURE__ */ jsx90(
42033
+ allowCanvasMovement && selection.capabilities.canApplyManualSize && /* @__PURE__ */ jsx90(
42082
42034
  "div",
42083
42035
  {
42084
42036
  className: "absolute -right-1.5 -bottom-1.5 w-3 h-3 rounded-sm bg-studio-accent border border-studio-accent/60",
@@ -42100,7 +42052,7 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
42100
42052
  },
42101
42053
  selectionKey
42102
42054
  ),
42103
- cropMode && /* @__PURE__ */ jsx90(
42055
+ selection.capabilities.canCrop && groupSelections.length <= 1 && /* @__PURE__ */ jsx90(
42104
42056
  DomEditCropHandles,
42105
42057
  {
42106
42058
  selection,
@@ -42162,10 +42114,10 @@ var DomEditOverlay = memo35(function DomEditOverlay2({
42162
42114
  });
42163
42115
 
42164
42116
  // src/components/editor/MotionPathOverlay.tsx
42165
- import { memo as memo36, useEffect as useEffect61, useRef as useRef90, useState as useState78 } from "react";
42117
+ import { memo as memo36, useEffect as useEffect60, useRef as useRef90, useState as useState78 } from "react";
42166
42118
 
42167
42119
  // src/contexts/DomEditContext.tsx
42168
- import { createContext as createContext6, useCallback as useCallback95, useContext as useContext6, useMemo as useMemo34, useRef as useRef88 } from "react";
42120
+ import { createContext as createContext6, useCallback as useCallback95, useContext as useContext6, useMemo as useMemo33, useRef as useRef88 } from "react";
42169
42121
  import { jsx as jsx91 } from "react/jsx-runtime";
42170
42122
  var DomEditActionsContext = createContext6(null);
42171
42123
  var DomEditSelectionContext = createContext6(null);
@@ -42270,7 +42222,7 @@ function DomEditProvider({
42270
42222
  (mutation, options) => commitMutationRef.current(mutation, options),
42271
42223
  []
42272
42224
  );
42273
- const actions = useMemo34(
42225
+ const actions = useMemo33(
42274
42226
  () => ({
42275
42227
  handleTimelineElementSelect,
42276
42228
  handlePreviewCanvasMouseDown,
@@ -42406,7 +42358,7 @@ function DomEditProvider({
42406
42358
  handleSetAllKeyframeEases
42407
42359
  ]
42408
42360
  );
42409
- const selection = useMemo34(
42361
+ const selection = useMemo33(
42410
42362
  () => ({
42411
42363
  domEditSelection,
42412
42364
  domEditGroupSelections,
@@ -42621,7 +42573,7 @@ function commitCreatePath(targetSelector, position, x, y, commit) {
42621
42573
  }
42622
42574
 
42623
42575
  // src/components/editor/useMotionPathData.ts
42624
- import { useEffect as useEffect60, useRef as useRef89, useState as useState77 } from "react";
42576
+ import { useEffect as useEffect59, useRef as useRef89, useState as useState77 } from "react";
42625
42577
  function transformTranslate(el) {
42626
42578
  const t = el.ownerDocument?.defaultView?.getComputedStyle(el).transform;
42627
42579
  if (!t || t === "none") return { x: 0, y: 0 };
@@ -42692,7 +42644,7 @@ function useMotionPathData(iframeRef, selector) {
42692
42644
  const [visibleInPreview, setVisibleInPreview] = useState77(true);
42693
42645
  const [home, setHome] = useState77(null);
42694
42646
  const [pScale, setPScale] = useState77(1);
42695
- useEffect60(() => {
42647
+ useEffect59(() => {
42696
42648
  if (!selector) {
42697
42649
  setRect(null);
42698
42650
  setHome(null);
@@ -42735,7 +42687,7 @@ function useMotionPathData(iframeRef, selector) {
42735
42687
  raf = requestAnimationFrame(tick);
42736
42688
  return () => cancelAnimationFrame(raf);
42737
42689
  }, [selector, iframeRef]);
42738
- useEffect60(() => {
42690
+ useEffect59(() => {
42739
42691
  if (!selector) {
42740
42692
  setGeometry(null);
42741
42693
  return;
@@ -42787,19 +42739,19 @@ var MotionPathOverlay = memo36(function MotionPathOverlay2({
42787
42739
  const dragRef = useRef90(null);
42788
42740
  const parkTimerRef = useRef90(void 0);
42789
42741
  const animId = editableAnimationId(selectedGsapAnimations ?? [], geometry?.kind ?? "linear");
42790
- useEffect61(() => () => clearTimeout(parkTimerRef.current), [animId]);
42742
+ useEffect60(() => () => clearTimeout(parkTimerRef.current), [animId]);
42791
42743
  const createMode = geometryResolved && !geometry && Boolean(selection?.element) && !isPlaying;
42792
42744
  const createSelector = createMode ? selectorFor(selection) : null;
42793
42745
  const compW = compositionSize?.width ?? null;
42794
42746
  const canCreate = createMode && hasMotionPathPlugin(iframeRef.current);
42795
- useEffect61(() => {
42747
+ useEffect60(() => {
42796
42748
  setMotionPathCreateAvailable(Boolean(canCreate));
42797
42749
  return () => setMotionPathCreateAvailable(false);
42798
42750
  }, [canCreate, setMotionPathCreateAvailable]);
42799
- useEffect61(() => {
42751
+ useEffect60(() => {
42800
42752
  if (armed && !canCreate) setMotionPathArmed(false);
42801
42753
  }, [armed, canCreate, setMotionPathArmed]);
42802
- useEffect61(() => {
42754
+ useEffect60(() => {
42803
42755
  if (!armed || !createSelector || !compW) return;
42804
42756
  const surface = iframeRef.current?.ownerDocument?.querySelector(
42805
42757
  "[data-preview-pan-surface]"
@@ -43113,8 +43065,8 @@ var MotionPathOverlay = memo36(function MotionPathOverlay2({
43113
43065
  });
43114
43066
 
43115
43067
  // src/components/editor/SnapToolbar.tsx
43116
- import { memo as memo37, useCallback as useCallback96, useEffect as useEffect62, useRef as useRef91, useState as useState79 } from "react";
43117
- import { Crop, MagnetStraight, GridFour, Path } from "@phosphor-icons/react";
43068
+ import { memo as memo37, useCallback as useCallback96, useEffect as useEffect61, useRef as useRef91, useState as useState79 } from "react";
43069
+ import { MagnetStraight, GridFour, Path } from "@phosphor-icons/react";
43118
43070
  import { jsx as jsx94, jsxs as jsxs79 } from "react/jsx-runtime";
43119
43071
  var SNAP_DEFAULTS = {
43120
43072
  snapEnabled: true,
@@ -43137,9 +43089,6 @@ var SnapToolbar = memo37(function SnapToolbar2({ onSnapChange }) {
43137
43089
  const motionPathCreateAvailable = usePlayerStore((s) => s.motionPathCreateAvailable);
43138
43090
  const motionPathArmed = usePlayerStore((s) => s.motionPathArmed);
43139
43091
  const setMotionPathArmed = usePlayerStore((s) => s.setMotionPathArmed);
43140
- const cropAvailable = usePlayerStore((s) => s.cropAvailable);
43141
- const cropMode = usePlayerStore((s) => s.cropMode);
43142
- const setCropMode = usePlayerStore((s) => s.setCropMode);
43143
43092
  const popoverRef = useRef91(null);
43144
43093
  const gridButtonRef = useRef91(null);
43145
43094
  const updatePrefs = useCallback96(
@@ -43159,7 +43108,7 @@ var SnapToolbar = memo37(function SnapToolbar2({ onSnapChange }) {
43159
43108
  const toggleGrid = useCallback96(() => {
43160
43109
  updatePrefs({ gridVisible: !prefs.gridVisible });
43161
43110
  }, [prefs.gridVisible, updatePrefs]);
43162
- useEffect62(() => {
43111
+ useEffect61(() => {
43163
43112
  const handleKeyDown = (e) => {
43164
43113
  if (e.defaultPrevented) return;
43165
43114
  const t = e.target;
@@ -43178,7 +43127,7 @@ var SnapToolbar = memo37(function SnapToolbar2({ onSnapChange }) {
43178
43127
  document.addEventListener("keydown", handleKeyDown);
43179
43128
  return () => document.removeEventListener("keydown", handleKeyDown);
43180
43129
  }, [updatePrefs]);
43181
- useEffect62(() => {
43130
+ useEffect61(() => {
43182
43131
  if (!gridPopoverOpen) return;
43183
43132
  const handleClickOutside = (e) => {
43184
43133
  const target = e.target;
@@ -43194,18 +43143,6 @@ var SnapToolbar = memo37(function SnapToolbar2({ onSnapChange }) {
43194
43143
  className: "absolute top-2 right-2 z-50 flex items-center gap-1",
43195
43144
  onPointerDown: (e) => e.stopPropagation(),
43196
43145
  children: [
43197
- cropAvailable && /* @__PURE__ */ jsx94(
43198
- "button",
43199
- {
43200
- type: "button",
43201
- className: `rounded-md p-1.5 transition-colors ${cropMode ? "bg-studio-accent/20 text-studio-accent" : "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"}`,
43202
- onClick: () => setCropMode(!cropMode),
43203
- title: cropMode ? "Exit crop (Esc)" : "Crop selection",
43204
- "aria-label": "Crop selection",
43205
- "aria-pressed": cropMode,
43206
- children: /* @__PURE__ */ jsx94(Crop, { size: 16, weight: cropMode ? "fill" : "regular" })
43207
- }
43208
- ),
43209
43146
  motionPathCreateAvailable && /* @__PURE__ */ jsx94(
43210
43147
  "button",
43211
43148
  {
@@ -43293,7 +43230,7 @@ var SnapToolbar = memo37(function SnapToolbar2({ onSnapChange }) {
43293
43230
  });
43294
43231
 
43295
43232
  // src/components/StudioFeedbackBar.tsx
43296
- import { memo as memo38, useState as useState80, useCallback as useCallback97, useRef as useRef92, useEffect as useEffect63 } from "react";
43233
+ import { memo as memo38, useState as useState80, useCallback as useCallback97, useRef as useRef92, useEffect as useEffect62 } from "react";
43297
43234
  import { Fragment as Fragment30, jsx as jsx95, jsxs as jsxs80 } from "react/jsx-runtime";
43298
43235
  var DEFAULT_FEEDBACK_INTERVAL = 10;
43299
43236
  var AUTO_DISMISS_MS2 = 2e4;
@@ -43355,7 +43292,7 @@ var StudioFeedbackBar = memo38(function StudioFeedbackBar2() {
43355
43292
  const [exiting, setExiting] = useState80(false);
43356
43293
  const inputRef = useRef92(null);
43357
43294
  const dismissTimerRef = useRef92(null);
43358
- useEffect63(() => {
43295
+ useEffect62(() => {
43359
43296
  incrementSessionCount();
43360
43297
  const showTimer = setTimeout(() => {
43361
43298
  if (shouldShowFeedback()) {
@@ -43364,12 +43301,12 @@ var StudioFeedbackBar = memo38(function StudioFeedbackBar2() {
43364
43301
  }, 3e3);
43365
43302
  return () => clearTimeout(showTimer);
43366
43303
  }, []);
43367
- useEffect63(() => {
43304
+ useEffect62(() => {
43368
43305
  if (!visible) return;
43369
43306
  const raf = requestAnimationFrame(() => setEntered(true));
43370
43307
  return () => cancelAnimationFrame(raf);
43371
43308
  }, [visible]);
43372
- useEffect63(() => {
43309
+ useEffect62(() => {
43373
43310
  if (!visible || rating !== null || submitted) return;
43374
43311
  dismissTimerRef.current = setTimeout(() => {
43375
43312
  handleDismiss();
@@ -43378,7 +43315,7 @@ var StudioFeedbackBar = memo38(function StudioFeedbackBar2() {
43378
43315
  if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
43379
43316
  };
43380
43317
  }, [visible, rating, submitted]);
43381
- useEffect63(() => {
43318
+ useEffect62(() => {
43382
43319
  if (rating !== null && inputRef.current) {
43383
43320
  inputRef.current.focus();
43384
43321
  }
@@ -43542,8 +43479,6 @@ function StudioPreviewArea({
43542
43479
  isGestureRecording,
43543
43480
  recordingState,
43544
43481
  onToggleRecording,
43545
- cropMode,
43546
- onCropModeChange,
43547
43482
  blockPreview,
43548
43483
  gestureOverlay
43549
43484
  }) {
@@ -43614,7 +43549,7 @@ function StudioPreviewArea({
43614
43549
  },
43615
43550
  [domEditSelection?.id, selectedGsapAnimations]
43616
43551
  );
43617
- const timelineEditCallbacks = useMemo35(
43552
+ const timelineEditCallbacks = useMemo34(
43618
43553
  () => ({
43619
43554
  onMoveElement: handleTimelineElementMove,
43620
43555
  onResizeElement: handleTimelineElementResize,
@@ -43790,8 +43725,6 @@ function StudioPreviewArea({
43790
43725
  onBoxSizeCommit: handleDomBoxSizeCommit,
43791
43726
  onRotationCommit: handleDomRotationCommit,
43792
43727
  onStyleCommit: handleDomStyleCommit,
43793
- cropMode,
43794
- onCropModeChange,
43795
43728
  gridVisible: snapPrefs.gridVisible,
43796
43729
  gridSpacing: snapPrefs.gridSpacing,
43797
43730
  recordingState,
@@ -43833,14 +43766,14 @@ function StudioPreviewArea({
43833
43766
  // src/components/StudioRightPanel.tsx
43834
43767
  import {
43835
43768
  useCallback as useCallback108,
43836
- useEffect as useEffect67,
43837
- useMemo as useMemo36,
43769
+ useEffect as useEffect66,
43770
+ useMemo as useMemo35,
43838
43771
  useRef as useRef97,
43839
43772
  useState as useState90
43840
43773
  } from "react";
43841
43774
 
43842
43775
  // src/components/editor/LayersPanel.tsx
43843
- import { memo as memo39, useState as useState83, useCallback as useCallback100, useEffect as useEffect64, useRef as useRef94 } from "react";
43776
+ import { memo as memo39, useState as useState83, useCallback as useCallback100, useEffect as useEffect63, useRef as useRef94 } from "react";
43844
43777
 
43845
43778
  // src/components/editor/useLayerDrag.ts
43846
43779
  import { useRef as useRef93, useState as useState82, useCallback as useCallback99 } from "react";
@@ -44063,10 +43996,10 @@ var LayersPanel = memo39(function LayersPanel2() {
44063
43996
  });
44064
43997
  setLayers(sortLayersByZIndex(items));
44065
43998
  }, [previewIframeRef, activeCompPath, isMasterView, activeGroupElement, setActiveGroupElement]);
44066
- useEffect64(() => {
43999
+ useEffect63(() => {
44067
44000
  collectLayers();
44068
44001
  }, [collectLayers, refreshKey]);
44069
- useEffect64(() => {
44002
+ useEffect63(() => {
44070
44003
  const iframe = previewIframeRef.current;
44071
44004
  if (!iframe) return;
44072
44005
  const handleLoad = () => {
@@ -44076,7 +44009,7 @@ var LayersPanel = memo39(function LayersPanel2() {
44076
44009
  iframe.addEventListener("load", handleLoad);
44077
44010
  return () => iframe.removeEventListener("load", handleLoad);
44078
44011
  }, [previewIframeRef, collectLayers]);
44079
- useEffect64(() => {
44012
+ useEffect63(() => {
44080
44013
  if (!compositionLoading) {
44081
44014
  const timer = setTimeout(collectLayers, 100);
44082
44015
  return () => clearTimeout(timer);
@@ -44951,7 +44884,7 @@ function ParamControl({
44951
44884
  }
44952
44885
 
44953
44886
  // src/components/renders/RenderQueue.tsx
44954
- import { memo as memo44, useState as useState87, useRef as useRef95, useEffect as useEffect65, useId as useId2 } from "react";
44887
+ import { memo as memo44, useState as useState87, useRef as useRef95, useEffect as useEffect64, useId as useId2 } from "react";
44955
44888
 
44956
44889
  // src/components/renders/RenderQueueItem.tsx
44957
44890
  import { memo as memo43, useCallback as useCallback104, useState as useState86 } from "react";
@@ -45250,8 +45183,8 @@ function FormatInfoTooltip({ format }) {
45250
45183
  const hide = () => {
45251
45184
  timeoutRef.current = setTimeout(() => setOpen(false), 120);
45252
45185
  };
45253
- useEffect65(() => () => clearTimeout(timeoutRef.current), []);
45254
- useEffect65(() => {
45186
+ useEffect64(() => () => clearTimeout(timeoutRef.current), []);
45187
+ useEffect64(() => {
45255
45188
  if (!open) return;
45256
45189
  const onKeyDown = (e) => {
45257
45190
  if (e.key === "Escape") setOpen(false);
@@ -45456,7 +45389,7 @@ var RenderQueue = memo44(function RenderQueue2({
45456
45389
  compositionDimensions
45457
45390
  }) {
45458
45391
  const listRef = useRef95(null);
45459
- useEffect65(() => {
45392
+ useEffect64(() => {
45460
45393
  if (listRef.current) {
45461
45394
  listRef.current.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
45462
45395
  }
@@ -45564,7 +45497,7 @@ var RenderQueue = memo44(function RenderQueue2({
45564
45497
  });
45565
45498
 
45566
45499
  // src/components/panels/SlideshowPanel.tsx
45567
- import { useState as useState89, useEffect as useEffect66, useCallback as useCallback106, useRef as useRef96 } from "react";
45500
+ import { useState as useState89, useEffect as useEffect65, useCallback as useCallback106, useRef as useRef96 } from "react";
45568
45501
  import { parseSlideshowManifest } from "@hyperframes/core/slideshow";
45569
45502
 
45570
45503
  // src/components/panels/SlideshowSubPanels.tsx
@@ -46203,7 +46136,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
46203
46136
  const { domEditSelection } = useDomEditSelectionContext();
46204
46137
  const manifestRef = useRef96(manifest);
46205
46138
  const notesCtrlRef = useRef96(makeSlideshowNotesController());
46206
- useEffect66(() => {
46139
+ useEffect65(() => {
46207
46140
  if (!compHtml) {
46208
46141
  notesCtrlRef.current.flush();
46209
46142
  setManifest({ slides: [] });
@@ -46237,7 +46170,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
46237
46170
  },
46238
46171
  [onPersistNotes]
46239
46172
  );
46240
- useEffect66(() => {
46173
+ useEffect65(() => {
46241
46174
  const ctrl = notesCtrlRef.current;
46242
46175
  return () => {
46243
46176
  ctrl.flush();
@@ -46775,8 +46708,6 @@ function StudioRightPanel({
46775
46708
  recordingState,
46776
46709
  recordingDuration,
46777
46710
  onToggleRecording,
46778
- cropMode,
46779
- onCropModeChange,
46780
46711
  sdkSession,
46781
46712
  reloadPreview,
46782
46713
  domEditSaveTimestampRef,
@@ -46879,7 +46810,7 @@ function StudioRightPanel({
46879
46810
  const splitContainerRef = useRef97(null);
46880
46811
  const splitDragRef = useRef97(null);
46881
46812
  const backgroundRemovalAbortRef = useRef97(null);
46882
- useEffect67(
46813
+ useEffect66(
46883
46814
  () => () => {
46884
46815
  backgroundRemovalAbortRef.current?.abort();
46885
46816
  },
@@ -46887,7 +46818,7 @@ function StudioRightPanel({
46887
46818
  );
46888
46819
  const renderJobs = renderQueue.jobs;
46889
46820
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
46890
- const slideshowScenes = useMemo36(() => {
46821
+ const slideshowScenes = useMemo35(() => {
46891
46822
  try {
46892
46823
  const win = previewIframeRef.current?.contentWindow;
46893
46824
  return (win?.__clipManifest?.scenes ?? []).map((s) => ({
@@ -47061,9 +46992,7 @@ function StudioRightPanel({
47061
46992
  onSetAllKeyframeEases: handleSetAllKeyframeEases,
47062
46993
  recordingState,
47063
46994
  recordingDuration,
47064
- onToggleRecording,
47065
- cropMode,
47066
- onCropModeChange
46995
+ onToggleRecording
47067
46996
  }
47068
46997
  );
47069
46998
  const renderQueuePanel = /* @__PURE__ */ jsx106(
@@ -47505,7 +47434,7 @@ function useEnableKeyframes(sessionRef) {
47505
47434
  }
47506
47435
 
47507
47436
  // src/hooks/useKeyframeKeyboard.ts
47508
- import { useEffect as useEffect68, useCallback as useCallback110 } from "react";
47437
+ import { useEffect as useEffect67, useCallback as useCallback110 } from "react";
47509
47438
  function isTextInput(el) {
47510
47439
  if (!el) return false;
47511
47440
  const tag = el.tagName;
@@ -47573,7 +47502,7 @@ function useKeyframeKeyboard({
47573
47502
  onNudgeKeyframe
47574
47503
  ]
47575
47504
  );
47576
- useEffect68(() => {
47505
+ useEffect67(() => {
47577
47506
  if (!enabled) return;
47578
47507
  window.addEventListener("keydown", handler, { capture: true });
47579
47508
  return () => window.removeEventListener("keydown", handler, { capture: true });
@@ -47834,15 +47763,19 @@ function TimelineToolbar({
47834
47763
  ] }) });
47835
47764
  }
47836
47765
 
47766
+ // src/components/storyboard/StoryboardView.tsx
47767
+ import { useState as useState96 } from "react";
47768
+ import { Copy as Copy2, Check as Check2 } from "@phosphor-icons/react";
47769
+
47837
47770
  // src/hooks/useStoryboard.ts
47838
- import { useCallback as useCallback111, useEffect as useEffect69, useState as useState91 } from "react";
47771
+ import { useCallback as useCallback111, useEffect as useEffect68, useState as useState91 } from "react";
47839
47772
  function useStoryboard(projectId) {
47840
47773
  const [data, setData] = useState91(null);
47841
47774
  const [loading, setLoading] = useState91(true);
47842
47775
  const [error, setError] = useState91(null);
47843
47776
  const [reloadKey, setReloadKey] = useState91(0);
47844
47777
  const reload = useCallback111(() => setReloadKey((k) => k + 1), []);
47845
- useEffect69(() => {
47778
+ useEffect68(() => {
47846
47779
  if (!projectId) return;
47847
47780
  let cancelled = false;
47848
47781
  setLoading(true);
@@ -47865,7 +47798,7 @@ function useStoryboard(projectId) {
47865
47798
  }
47866
47799
 
47867
47800
  // src/components/storyboard/StoryboardLoaded.tsx
47868
- import { useMemo as useMemo38, useState as useState95 } from "react";
47801
+ import { useMemo as useMemo37, useState as useState95 } from "react";
47869
47802
 
47870
47803
  // src/components/storyboard/StoryboardDirection.tsx
47871
47804
  import { jsx as jsx108, jsxs as jsxs93 } from "react/jsx-runtime";
@@ -47888,11 +47821,11 @@ function StoryboardDirection({ globals, frameCount }) {
47888
47821
  }
47889
47822
 
47890
47823
  // src/components/storyboard/FramePoster.tsx
47891
- import { useEffect as useEffect70, useState as useState92 } from "react";
47824
+ import { useEffect as useEffect69, useState as useState92 } from "react";
47892
47825
  import { jsx as jsx109 } from "react/jsx-runtime";
47893
47826
  function FramePoster({ projectId, src, seconds, title, fit = "cover" }) {
47894
47827
  const [failed2, setFailed] = useState92(false);
47895
- useEffect70(() => setFailed(false), [src, seconds]);
47828
+ useEffect69(() => setFailed(false), [src, seconds]);
47896
47829
  if (failed2) {
47897
47830
  return /* @__PURE__ */ jsx109("div", { className: "flex h-full w-full items-center justify-center text-[11px] text-neutral-600", children: "Preview unavailable" });
47898
47831
  }
@@ -48065,7 +47998,7 @@ function StoryboardScriptPanel({ script }) {
48065
47998
  }
48066
47999
 
48067
48000
  // src/components/storyboard/StoryboardSourceEditor.tsx
48068
- import { useCallback as useCallback112, useEffect as useEffect71, useMemo as useMemo37, useRef as useRef99, useState as useState93 } from "react";
48001
+ import { useCallback as useCallback112, useEffect as useEffect70, useMemo as useMemo36, useRef as useRef99, useState as useState93 } from "react";
48069
48002
  import { marked } from "marked";
48070
48003
  import DOMPurify from "dompurify";
48071
48004
  import { jsx as jsx114, jsxs as jsxs97 } from "react/jsx-runtime";
@@ -48077,7 +48010,7 @@ function useEditableFile(path, onSaved) {
48077
48010
  const [loading, setLoading] = useState93(true);
48078
48011
  const [saving, setSaving] = useState93(false);
48079
48012
  const [error, setError] = useState93(null);
48080
- useEffect71(() => {
48013
+ useEffect70(() => {
48081
48014
  if (!path) return;
48082
48015
  let cancelled = false;
48083
48016
  setLoading(true);
@@ -48115,7 +48048,7 @@ function hardenLinks(node) {
48115
48048
  function useMarkdownPreview(source) {
48116
48049
  const [debounced, setDebounced] = useState93(source);
48117
48050
  const primed = useRef99(false);
48118
- useEffect71(() => {
48051
+ useEffect70(() => {
48119
48052
  if (!primed.current && source !== "") {
48120
48053
  primed.current = true;
48121
48054
  setDebounced(source);
@@ -48124,7 +48057,7 @@ function useMarkdownPreview(source) {
48124
48057
  const id = window.setTimeout(() => setDebounced(source), 200);
48125
48058
  return () => window.clearTimeout(id);
48126
48059
  }, [source]);
48127
- return useMemo37(() => {
48060
+ return useMemo36(() => {
48128
48061
  const html2 = marked.parse(debounced, { async: false });
48129
48062
  DOMPurify.addHook("afterSanitizeAttributes", hardenLinks);
48130
48063
  try {
@@ -48147,8 +48080,8 @@ function StoryboardSourceEditor({
48147
48080
  const activePath = files.some((f) => f.path === selected) ? selected : files[0]?.path ?? "";
48148
48081
  const file = useEditableFile(activePath, onSaved);
48149
48082
  const previewHtml = useMarkdownPreview(file.content);
48150
- useEffect71(() => onDirtyChange?.(file.dirty), [onDirtyChange, file.dirty]);
48151
- useEffect71(() => {
48083
+ useEffect70(() => onDirtyChange?.(file.dirty), [onDirtyChange, file.dirty]);
48084
+ useEffect70(() => {
48152
48085
  if (!file.dirty) return;
48153
48086
  const onBeforeUnload = (event) => {
48154
48087
  event.preventDefault();
@@ -48220,7 +48153,7 @@ function StoryboardSourceEditor({
48220
48153
  }
48221
48154
 
48222
48155
  // src/components/storyboard/StoryboardFrameFocus.tsx
48223
- import { useCallback as useCallback113, useEffect as useEffect72, useState as useState94 } from "react";
48156
+ import { useCallback as useCallback113, useEffect as useEffect71, useState as useState94 } from "react";
48224
48157
  import { setFrameStatus, setFrameVoiceover } from "@hyperframes/core/storyboard";
48225
48158
  import { jsx as jsx115, jsxs as jsxs98 } from "react/jsx-runtime";
48226
48159
  function StoryboardFrameFocus({
@@ -48261,7 +48194,7 @@ function StoryboardFrameFocus({
48261
48194
  const saveVoiceover = useCallback113(() => {
48262
48195
  return applyEdit((src) => setFrameVoiceover(src, frame.index, draft));
48263
48196
  }, [applyEdit, frame.index, draft]);
48264
- useEffect72(() => {
48197
+ useEffect71(() => {
48265
48198
  if (!dirty) return;
48266
48199
  const onBeforeUnload = (e) => {
48267
48200
  e.preventDefault();
@@ -48276,7 +48209,7 @@ function StoryboardFrameFocus({
48276
48209
  const handleNavigate = (delta) => {
48277
48210
  if (confirmLeave()) onNavigate(delta);
48278
48211
  };
48279
- useEffect72(() => {
48212
+ useEffect71(() => {
48280
48213
  const onKey = (e) => {
48281
48214
  const el = document.activeElement;
48282
48215
  if (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement) return;
@@ -48454,7 +48387,7 @@ function StoryboardLoaded({
48454
48387
  const [subView, setSubView] = useState95("board");
48455
48388
  const [sourceDirty, setSourceDirty] = useState95(false);
48456
48389
  const [focusedIndex, setFocusedIndex] = useState95(null);
48457
- const sourceFiles = useMemo38(() => {
48390
+ const sourceFiles = useMemo37(() => {
48458
48391
  const files = [{ path: data.path, label: data.path }];
48459
48392
  if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
48460
48393
  return files;
@@ -48584,14 +48517,75 @@ function Message({ children, tone = "muted" }) {
48584
48517
  }
48585
48518
  );
48586
48519
  }
48520
+ function handoffPrompt(path) {
48521
+ return `Create a \`${path}\` at the project root to plan this video frame by frame.
48522
+
48523
+ Use this format:
48524
+
48525
+ ---
48526
+ format: 1920x1080
48527
+ message: <the one-line takeaway of the video>
48528
+ arc: <the narrative shape, e.g. Problem \u2192 Solution>
48529
+ audience: <who it's for>
48530
+ ---
48531
+
48532
+ ## Frame 1 \u2014 <title>
48533
+ - duration: 5s
48534
+ - transition_in: crossfade
48535
+ - status: planned
48536
+ - src: compositions/frames/01-<slug>.html
48537
+
48538
+ <A sentence or two: what's on screen and what the narration says.>
48539
+
48540
+ Add one \`## Frame N\` section per beat. Keep the arc tight.`;
48541
+ }
48587
48542
  function EmptyState({ path }) {
48588
- return /* @__PURE__ */ jsxs100("div", { className: "rounded-lg border border-dashed border-neutral-800 px-6 py-16 text-center", children: [
48589
- /* @__PURE__ */ jsx117("h2", { className: "text-base font-semibold text-neutral-300", children: "No storyboard yet" }),
48590
- /* @__PURE__ */ jsxs100("p", { className: "mx-auto mt-2 max-w-md text-sm text-neutral-500", children: [
48591
- "Add a ",
48592
- /* @__PURE__ */ jsx117("code", { className: "rounded bg-neutral-900 px-1 py-0.5 text-neutral-400", children: path }),
48593
- " at the project root to plan this video frame by frame. Your agent can create and iterate on it for you."
48594
- ] })
48543
+ const [copied, setCopied] = useState96(false);
48544
+ const prompt = handoffPrompt(path);
48545
+ const onCopy = async () => {
48546
+ if (await copyTextToClipboard(prompt)) {
48547
+ setCopied(true);
48548
+ window.setTimeout(() => setCopied(false), 2e3);
48549
+ }
48550
+ };
48551
+ return /* @__PURE__ */ jsxs100("div", { children: [
48552
+ /* @__PURE__ */ jsxs100("div", { className: "rounded-lg border border-dashed border-neutral-800 px-6 py-10 text-center", children: [
48553
+ /* @__PURE__ */ jsx117("h2", { className: "text-base font-semibold text-neutral-300", children: "No storyboard yet" }),
48554
+ /* @__PURE__ */ jsxs100("p", { className: "mx-auto mt-2 max-w-md text-sm text-neutral-500", children: [
48555
+ "Add a ",
48556
+ /* @__PURE__ */ jsx117("code", { className: "rounded bg-neutral-900 px-1 py-0.5 text-neutral-400", children: path }),
48557
+ " ",
48558
+ "at the project root to plan this video frame by frame. Hand this prompt to your coding agent to scaffold it."
48559
+ ] }),
48560
+ /* @__PURE__ */ jsxs100("div", { className: "mx-auto mt-6 max-w-2xl overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900 text-left", children: [
48561
+ /* @__PURE__ */ jsxs100("div", { className: "flex items-center justify-between border-b border-neutral-800 px-3 py-2", children: [
48562
+ /* @__PURE__ */ jsx117("span", { className: "font-mono text-xs text-neutral-500", children: "Prompt for your agent" }),
48563
+ /* @__PURE__ */ jsx117(
48564
+ Button,
48565
+ {
48566
+ size: "sm",
48567
+ variant: "secondary",
48568
+ onClick: onCopy,
48569
+ icon: copied ? /* @__PURE__ */ jsx117(Check2, { size: 14 }) : /* @__PURE__ */ jsx117(Copy2, { size: 14 }),
48570
+ children: copied ? "Copied" : "Copy prompt"
48571
+ }
48572
+ )
48573
+ ] }),
48574
+ /* @__PURE__ */ jsx117("pre", { className: "max-h-64 overflow-auto px-4 py-3 font-mono text-xs leading-relaxed text-neutral-400 whitespace-pre-wrap", children: prompt })
48575
+ ] })
48576
+ ] }),
48577
+ /* @__PURE__ */ jsx117(SkeletonPreview, {})
48578
+ ] });
48579
+ }
48580
+ function SkeletonPreview() {
48581
+ return /* @__PURE__ */ jsxs100("div", { "aria-hidden": "true", className: "mt-10 select-none opacity-40", children: [
48582
+ /* @__PURE__ */ jsx117("div", { className: "mb-4 text-center text-xs uppercase tracking-wide text-neutral-600", children: "Preview" }),
48583
+ /* @__PURE__ */ jsx117("div", { className: "grid gap-x-6 gap-y-8 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs100("div", { className: "rounded-lg border border-neutral-800 bg-neutral-900/40 p-3", children: [
48584
+ /* @__PURE__ */ jsx117("div", { className: "aspect-video w-full rounded bg-neutral-800/60" }),
48585
+ /* @__PURE__ */ jsx117("div", { className: "mt-3 h-3 w-2/3 rounded bg-neutral-800/60" }),
48586
+ /* @__PURE__ */ jsx117("div", { className: "mt-2 h-2.5 w-full rounded bg-neutral-800/40" }),
48587
+ /* @__PURE__ */ jsx117("div", { className: "mt-1.5 h-2.5 w-4/5 rounded bg-neutral-800/40" })
48588
+ ] }, i)) })
48595
48589
  ] });
48596
48590
  }
48597
48591
 
@@ -48612,11 +48606,11 @@ function StudioSplash({ waiting }) {
48612
48606
  }
48613
48607
 
48614
48608
  // src/hooks/useServerConnection.ts
48615
- import { useEffect as useEffect73, useState as useState96 } from "react";
48609
+ import { useEffect as useEffect72, useState as useState97 } from "react";
48616
48610
  function useServerConnection() {
48617
- const [projectId, setProjectId] = useState96(null);
48618
- const [resolving, setResolving] = useState96(true);
48619
- const [waitingForServer, setWaitingForServer] = useState96(false);
48611
+ const [projectId, setProjectId] = useState97(null);
48612
+ const [resolving, setResolving] = useState97(true);
48613
+ const [waitingForServer, setWaitingForServer] = useState97(false);
48620
48614
  useMountEffect(() => {
48621
48615
  const hashProjectId = parseProjectIdFromHash(window.location.hash);
48622
48616
  let cancelled = false;
@@ -48653,7 +48647,7 @@ function useServerConnection() {
48653
48647
  if (retryTimer !== null) clearTimeout(retryTimer);
48654
48648
  };
48655
48649
  });
48656
- useEffect73(() => {
48650
+ useEffect72(() => {
48657
48651
  const onHashChange = () => {
48658
48652
  const next = parseProjectIdFromHash(window.location.hash);
48659
48653
  if (next && next !== projectId) setProjectId(next);
@@ -48670,23 +48664,22 @@ function StudioApp() {
48670
48664
  const { projectId, resolving, waitingForServer } = useServerConnection();
48671
48665
  const initialUrlStateRef = useRef100(readStudioUrlStateFromWindow());
48672
48666
  const viewModeValue = useViewModeState();
48673
- useEffect74(() => {
48667
+ useEffect73(() => {
48674
48668
  if (resolving || waitingForServer) return;
48675
48669
  if (hasFiredSessionStart()) return;
48676
48670
  markSessionStartFired();
48677
48671
  trackStudioSessionStart({ has_project: projectId != null });
48678
48672
  }, [projectId, resolving, waitingForServer]);
48679
- const [activeCompPath, setActiveCompPath] = useState97(null);
48680
- const [activeCompPathHydrated, setActiveCompPathHydrated] = useState97(
48673
+ const [activeCompPath, setActiveCompPath] = useState98(null);
48674
+ const [activeCompPathHydrated, setActiveCompPathHydrated] = useState98(
48681
48675
  () => initialUrlStateRef.current.activeCompPath == null
48682
48676
  );
48683
- const [compIdToSrc, setCompIdToSrc] = useState97(/* @__PURE__ */ new Map());
48684
- const [previewIframe, setPreviewIframe] = useState97(null);
48685
- const [compositionLoading, setCompositionLoading] = useState97(true);
48686
- const [refreshKey, setRefreshKey] = useState97(0);
48687
- const [previewDocumentVersion, setPreviewDocumentVersion] = useState97(0);
48688
- const [blockPreview, setBlockPreview] = useState97(null);
48689
- const cropModeProps = useCropModeProps();
48677
+ const [compIdToSrc, setCompIdToSrc] = useState98(/* @__PURE__ */ new Map());
48678
+ const [previewIframe, setPreviewIframe] = useState98(null);
48679
+ const [compositionLoading, setCompositionLoading] = useState98(true);
48680
+ const [refreshKey, setRefreshKey] = useState98(0);
48681
+ const [previewDocumentVersion, setPreviewDocumentVersion] = useState98(0);
48682
+ const [blockPreview, setBlockPreview] = useState98(null);
48690
48683
  const previewIframeRef = useRef100(null);
48691
48684
  const activeCompPathRef = useRef100(activeCompPath);
48692
48685
  activeCompPathRef.current = activeCompPath;
@@ -48701,7 +48694,7 @@ function StudioApp() {
48701
48694
  const isPlaying = usePlayerStore((s) => s.isPlaying);
48702
48695
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
48703
48696
  const activePreviewUrl = activeCompPath ? `/api/projects/${projectId}/preview/comp/${activeCompPath}` : null;
48704
- const effectiveTimelineDuration = useMemo39(() => {
48697
+ const effectiveTimelineDuration = useMemo38(() => {
48705
48698
  const maxEnd = timelineElements.length > 0 ? Math.max(...timelineElements.map((el) => el.start + el.duration)) : 0;
48706
48699
  return Math.max(timelineDuration, maxEnd);
48707
48700
  }, [timelineDuration, timelineElements]);
@@ -48715,13 +48708,13 @@ function StudioApp() {
48715
48708
  window.setTimeout(() => setPreviewDocumentVersion((v) => v + 1), 300)
48716
48709
  );
48717
48710
  }, []);
48718
- useEffect74(
48711
+ useEffect73(
48719
48712
  () => () => {
48720
48713
  for (const id of refreshTimersRef.current) clearTimeout(id);
48721
48714
  },
48722
48715
  []
48723
48716
  );
48724
- const [timelineVisible, setTimelineVisible] = useState97(
48717
+ const [timelineVisible, setTimelineVisible] = useState98(
48725
48718
  () => initialUrlStateRef.current.timelineVisible ?? readStudioUiPreferences().timelineVisible ?? true
48726
48719
  );
48727
48720
  const toggleTimelineVisibility = useCallback114(() => {
@@ -48748,7 +48741,7 @@ function StudioApp() {
48748
48741
  setRefreshKey
48749
48742
  });
48750
48743
  const sdkHandle = useSdkSession(projectId, activeCompPath, domEditSaveTimestampRef);
48751
- useEffect74(() => {
48744
+ useEffect73(() => {
48752
48745
  if (activeCompPathHydrated) return;
48753
48746
  if (!fileManager.fileTreeLoaded) return;
48754
48747
  const nextCompPath = normalizeStudioCompositionPath(
@@ -49047,7 +49040,7 @@ function StudioApp() {
49047
49040
  timelineVisible,
49048
49041
  toggleTimelineVisibility
49049
49042
  });
49050
- const timelineToolbar = useMemo39(
49043
+ const timelineToolbar = useMemo38(
49051
49044
  () => /* @__PURE__ */ jsx119(
49052
49045
  TimelineToolbar,
49053
49046
  {
@@ -49143,7 +49136,6 @@ function StudioApp() {
49143
49136
  recordingState: gestureState,
49144
49137
  onToggleRecording: recordingToggle,
49145
49138
  blockPreview,
49146
- ...cropModeProps,
49147
49139
  gestureOverlay: gestureState === "recording" && previewIframe ? /* @__PURE__ */ jsx119(
49148
49140
  GestureTrailOverlay,
49149
49141
  {
@@ -49173,7 +49165,6 @@ function StudioApp() {
49173
49165
  reloadPreview,
49174
49166
  domEditSaveTimestampRef,
49175
49167
  recordEdit: editHistory.recordEdit,
49176
- ...cropModeProps,
49177
49168
  onToggleElementHidden: timelineEditing.handleToggleElementHidden
49178
49169
  }
49179
49170
  )
@@ -49202,10 +49193,10 @@ function StudioApp() {
49202
49193
  }
49203
49194
 
49204
49195
  // src/hooks/useElementPicker.ts
49205
- import { useState as useState98, useCallback as useCallback115, useRef as useRef101 } from "react";
49196
+ import { useState as useState99, useCallback as useCallback115, useRef as useRef101 } from "react";
49206
49197
  function useElementPicker(iframeRef, options) {
49207
- const [isPickMode, setIsPickMode] = useState98(false);
49208
- const [pickedElement, setPickedElement] = useState98(null);
49198
+ const [isPickMode, setIsPickMode] = useState99(false);
49199
+ const [pickedElement, setPickedElement] = useState99(null);
49209
49200
  const activeOverrideRef = useRef101(null);
49210
49201
  const getActiveIframe = useCallback115(() => {
49211
49202
  return activeOverrideRef.current ?? iframeRef.current;