hyperframes 0.8.85 → 0.8.86

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.
@@ -13,6 +13,7 @@ import {
13
13
  applyPreviewVariablesToUrl,
14
14
  buildTimelineElementIdentity,
15
15
  buildTimelineElementKey,
16
+ computePinnedZoomPercent,
16
17
  createTimelineDomNodeResolver,
17
18
  createTimelineResetState,
18
19
  deriveTimelineStoreKey,
@@ -65,7 +66,7 @@ import {
65
66
  whenPreviewBooted,
66
67
  wrapTimeline,
67
68
  writeStudioUiPreferences
68
- } from "./chunk-7VDQIGOU.js";
69
+ } from "./chunk-7PLTC6SV.js";
69
70
  import {
70
71
  Button,
71
72
  ContextMenu,
@@ -2048,20 +2049,20 @@ function getTimelineInsertBoundaryBand(rowHeight) {
2048
2049
  var DRAG_EXTEND_MARGIN_PX = 160;
2049
2050
  var MIN_TIMELINE_EXTENT_S = 60;
2050
2051
  var FIT_ZOOM_HEADROOM = 1.2;
2052
+ function getTimelineFitSpan(effectiveDuration) {
2053
+ return Number.isFinite(effectiveDuration) && effectiveDuration > 0 ? effectiveDuration * FIT_ZOOM_HEADROOM : MIN_TIMELINE_EXTENT_S;
2054
+ }
2051
2055
  function getTimelineFitPps(viewportWidth, effectiveDuration, contentOrigin) {
2052
- const safeDuration = Number.isFinite(effectiveDuration) && effectiveDuration > 0 ? effectiveDuration : 0;
2053
- const span = Math.max(safeDuration * FIT_ZOOM_HEADROOM, MIN_TIMELINE_EXTENT_S);
2054
2056
  if (!Number.isFinite(viewportWidth) || viewportWidth <= contentOrigin) return 100;
2055
- return (viewportWidth - contentOrigin - 2) / span;
2057
+ return (viewportWidth - contentOrigin - 2) / getTimelineFitSpan(effectiveDuration);
2056
2058
  }
2057
2059
  function getTimelineDisplayContentWidth(input) {
2058
2060
  const safePps = Number.isFinite(input.pps) ? Math.max(input.pps, 0) : 0;
2059
2061
  return Math.max(
2060
- input.trackContentWidth,
2061
2062
  input.viewportWidth - input.contentOrigin - 2,
2062
2063
  input.dragGhostEndPx ?? 0,
2063
2064
  input.resizeGhostEndPx ?? 0,
2064
- MIN_TIMELINE_EXTENT_S * safePps
2065
+ getTimelineFitSpan(input.effectiveDuration) * safePps
2065
2066
  );
2066
2067
  }
2067
2068
  function getTimelineContentXFromClient(input) {
@@ -2887,7 +2888,7 @@ function useTimelinePlayhead({
2887
2888
  );
2888
2889
  useEffect5(() => {
2889
2890
  syncPlayheadPosition(currentTime);
2890
- }, [currentTime, pps, syncPlayheadPosition]);
2891
+ }, [currentTime, pps, syncPlayheadPosition, timelineReady, elementsLength]);
2891
2892
  useLayoutEffect(() => {
2892
2893
  const scroll = scrollRef.current;
2893
2894
  if (!scroll || zoomMode !== "fit") return;
@@ -7055,7 +7056,7 @@ function useTimelineStackingSync({
7055
7056
  }
7056
7057
 
7057
7058
  // src/player/components/useTimelineGeometry.ts
7058
- import { useEffect as useEffect12, useRef as useRef13 } from "react";
7059
+ import { useEffect as useEffect12, useLayoutEffect as useLayoutEffect2, useRef as useRef13 } from "react";
7059
7060
  function useTimelineGeometry({
7060
7061
  viewportWidth,
7061
7062
  effectiveDuration,
@@ -7074,11 +7075,10 @@ function useTimelineGeometry({
7074
7075
  const fitPps = getTimelineFitPps(viewportWidth, effectiveDuration, contentOrigin);
7075
7076
  const pps = getTimelinePixelsPerSecond(fitPps, zoomMode, manualZoomPercent);
7076
7077
  ppsRef.current = pps;
7077
- const trackContentWidth = Math.max(0, effectiveDuration * pps);
7078
7078
  const dragGhostEndPx = draggedClip?.started ? (draggedClip.previewStart + draggedClip.element.duration) * pps + DRAG_EXTEND_MARGIN_PX : 0;
7079
7079
  const resizeGhostEndPx = resizingClip?.started ? (resizingClip.previewStart + resizingClip.previewDuration) * pps + DRAG_EXTEND_MARGIN_PX : 0;
7080
7080
  const displayContentWidth = getTimelineDisplayContentWidth({
7081
- trackContentWidth,
7081
+ effectiveDuration,
7082
7082
  viewportWidth,
7083
7083
  contentOrigin,
7084
7084
  pps,
@@ -7107,6 +7107,15 @@ function useTimelineGeometry({
7107
7107
  useEffect12(() => {
7108
7108
  usePlayerStore.getState().setTimelineScale(pps, fitPps);
7109
7109
  }, [pps, fitPps]);
7110
+ const hasClips = expandedElements.length > 0;
7111
+ const lastScale = useRef13({ effectiveDuration, pps, hasClips });
7112
+ useLayoutEffect2(() => {
7113
+ const last = lastScale.current;
7114
+ lastScale.current = { effectiveDuration, pps, hasClips };
7115
+ if (zoomMode !== "manual" || !last.hasClips || !hasClips) return;
7116
+ if (last.effectiveDuration === effectiveDuration) return;
7117
+ usePlayerStore.setState({ manualZoomPercent: computePinnedZoomPercent(last.pps, fitPps) });
7118
+ });
7110
7119
  return {
7111
7120
  pps,
7112
7121
  fitPps,
@@ -11800,7 +11809,7 @@ function useTimelineClipRenderWindow({
11800
11809
  }
11801
11810
 
11802
11811
  // src/player/components/useTimelineActiveClips.ts
11803
- import { useCallback as useCallback16, useLayoutEffect as useLayoutEffect2, useRef as useRef22 } from "react";
11812
+ import { useCallback as useCallback16, useLayoutEffect as useLayoutEffect3, useRef as useRef22 } from "react";
11804
11813
  function isTimelineIntervalActive(interval, time) {
11805
11814
  return Number.isFinite(time) && !interval.hidden && Number.isFinite(interval.start) && Number.isFinite(interval.end) && time >= interval.start && time <= interval.end;
11806
11815
  }
@@ -11867,7 +11876,7 @@ function useTimelineActiveClips({
11867
11876
  },
11868
11877
  [scrollRef]
11869
11878
  );
11870
- useLayoutEffect2(() => {
11879
+ useLayoutEffect3(() => {
11871
11880
  refreshRecords(currentTime);
11872
11881
  }, [clipStateVersion, currentTime, elementStateVersion, refreshRecords]);
11873
11882
  useMountEffect(
@@ -11885,7 +11894,7 @@ function useTimelineLaneMoveRefresh() {
11885
11894
  }
11886
11895
 
11887
11896
  // src/player/components/useTimelineFocusCoordinator.ts
11888
- import { useEffect as useEffect18, useLayoutEffect as useLayoutEffect3, useMemo as useMemo17, useRef as useRef23 } from "react";
11897
+ import { useEffect as useEffect18, useLayoutEffect as useLayoutEffect4, useMemo as useMemo17, useRef as useRef23 } from "react";
11889
11898
 
11890
11899
  // src/player/components/timelineKeyboardNavigation.ts
11891
11900
  var NAVIGATION_KEYS = /* @__PURE__ */ new Set([
@@ -12305,7 +12314,7 @@ function useTimelineFocusCoordinator({
12305
12314
  resolvedRef.current = null;
12306
12315
  return null;
12307
12316
  }, [logicalRows, projectId, request, sessionEpoch]);
12308
- useLayoutEffect3(() => {
12317
+ useLayoutEffect4(() => {
12309
12318
  previousRowsRef.current = logicalRows;
12310
12319
  }, [logicalRows]);
12311
12320
  useEffect18(() => {
@@ -12423,7 +12432,7 @@ function useTimelineLogicalRows({
12423
12432
  // src/player/components/useTimelineRowVirtualization.ts
12424
12433
  import {
12425
12434
  useCallback as useCallback19,
12426
- useLayoutEffect as useLayoutEffect4,
12435
+ useLayoutEffect as useLayoutEffect5,
12427
12436
  useMemo as useMemo20,
12428
12437
  useRef as useRef24,
12429
12438
  useState as useState14
@@ -12603,7 +12612,7 @@ function useTimelineRowVirtualization({
12603
12612
  });
12604
12613
  const previousLayoutRef = useRef24(rowGeometry);
12605
12614
  const previousSessionEpochRef = useRef24(sessionEpoch);
12606
- useLayoutEffect4(() => {
12615
+ useLayoutEffect5(() => {
12607
12616
  const scroll = scrollRef.current;
12608
12617
  const previousGeometry = previousLayoutRef.current;
12609
12618
  if (previousSessionEpochRef.current !== sessionEpoch) {
@@ -22348,7 +22357,7 @@ var Timeline = Object.assign(TimelineComposed, {
22348
22357
  import { memo as memo17, useMemo as useMemo30 } from "react";
22349
22358
 
22350
22359
  // src/hooks/useThumbnailLease.ts
22351
- import { useCallback as useCallback31, useLayoutEffect as useLayoutEffect5, useRef as useRef40, useSyncExternalStore as useSyncExternalStore3 } from "react";
22360
+ import { useCallback as useCallback31, useLayoutEffect as useLayoutEffect6, useRef as useRef40, useSyncExternalStore as useSyncExternalStore3 } from "react";
22352
22361
 
22353
22362
  // src/player/lib/thumbnailScheduler.ts
22354
22363
  var PRIORITY_SCORE = {
@@ -22744,7 +22753,7 @@ function useThumbnailLease(request, scheduler = thumbnailScheduler) {
22744
22753
  const current = requestRef.current;
22745
22754
  return current && identity !== null ? scheduler.getSnapshot(current) : IDLE;
22746
22755
  }, [identity, scheduler]);
22747
- useLayoutEffect5(() => {
22756
+ useLayoutEffect6(() => {
22748
22757
  if (priority) leaseRef.current?.updatePriority(priority);
22749
22758
  }, [priority]);
22750
22759
  return useSyncExternalStore3(subscribe, getSnapshot, () => IDLE);
@@ -26088,23 +26097,33 @@ function useCompositionStack({
26088
26097
  ]);
26089
26098
  const onCompositionChangeRef = useRef46(onCompositionChange);
26090
26099
  onCompositionChangeRef.current = onCompositionChange;
26100
+ const masterSeekRef = useRef46(0);
26101
+ const masterSeekProjectRef = useRef46(null);
26102
+ const projectIdRef = useRef46(projectId);
26103
+ projectIdRef.current = projectId;
26104
+ const stackRef = useRef46(compositionStack);
26091
26105
  const updateCompositionStack = useCallback38((action) => {
26092
- setCompositionStack((prev) => {
26093
- const next = typeof action === "function" ? action(prev) : action;
26094
- const id = next[next.length - 1]?.id;
26095
- queueMicrotask(() => onCompositionChangeRef.current?.(id === "master" ? null : id));
26096
- return next;
26097
- });
26106
+ const prev = stackRef.current;
26107
+ const next = typeof action === "function" ? action(prev) : action;
26108
+ stackRef.current = next;
26109
+ const player = usePlayerStore.getState();
26110
+ if (prev.length === 1 && next.length > 1) {
26111
+ masterSeekRef.current = player.isPlaying ? liveTime.latest() : player.currentTime;
26112
+ masterSeekProjectRef.current = projectIdRef.current;
26113
+ } else if (next.length === 1 && prev.length > 1) {
26114
+ if (masterSeekProjectRef.current === projectIdRef.current) {
26115
+ player.setCurrentTime(masterSeekRef.current);
26116
+ }
26117
+ }
26118
+ setCompositionStack(next);
26119
+ const id = next[next.length - 1]?.id;
26120
+ queueMicrotask(() => onCompositionChangeRef.current?.(id === "master" ? null : id));
26098
26121
  }, []);
26099
- const masterSeekRef = useRef46(0);
26100
26122
  const [compIdToSrc, setCompIdToSrc] = useState28(/* @__PURE__ */ new Map());
26101
26123
  const compIdToSrcRef = useRef46(compIdToSrc);
26102
26124
  compIdToSrcRef.current = compIdToSrc;
26103
26125
  const handleNavigateComposition = useCallback38(
26104
26126
  (index) => {
26105
- if (index === 0 && masterSeekRef.current > 0) {
26106
- usePlayerStore.getState().setCurrentTime(masterSeekRef.current);
26107
- }
26108
26127
  usePlayerStore.getState().setElements([]);
26109
26128
  updateCompositionStack((prev) => prev.slice(0, index + 1));
26110
26129
  },
@@ -26114,7 +26133,6 @@ function useCompositionStack({
26114
26133
  const handleDrillDown = useCallback38(
26115
26134
  (element) => {
26116
26135
  if (!element.compositionSrc) return;
26117
- masterSeekRef.current = usePlayerStore.getState().currentTime;
26118
26136
  const compId = element.id;
26119
26137
  let resolvedPath = compIdToSrcRef.current.get(compId);
26120
26138
  if (!resolvedPath) {
@@ -26480,7 +26498,7 @@ import {
26480
26498
  memo as memo19,
26481
26499
  useCallback as useCallback44,
26482
26500
  useEffect as useEffect34,
26483
- useLayoutEffect as useLayoutEffect6,
26501
+ useLayoutEffect as useLayoutEffect7,
26484
26502
  useRef as useRef51,
26485
26503
  useState as useState31
26486
26504
  } from "react";
@@ -26978,7 +26996,7 @@ var NLEPreview = memo19(function NLEPreview2({
26978
26996
  [drawNavigator]
26979
26997
  );
26980
26998
  const zoomProjectRef = useRef51(projectId);
26981
- useLayoutEffect6(() => {
26999
+ useLayoutEffect7(() => {
26982
27000
  if (zoomProjectRef.current === projectId) return;
26983
27001
  zoomProjectRef.current = projectId;
26984
27002
  zoomRef.current = DEFAULT_PREVIEW_ZOOM;
@@ -38179,7 +38197,7 @@ function ConnectedDomEditOverlay({
38179
38197
  // src/components/editor/TopologyLens.tsx
38180
38198
  import {
38181
38199
  useEffect as useEffect49,
38182
- useLayoutEffect as useLayoutEffect7,
38200
+ useLayoutEffect as useLayoutEffect8,
38183
38201
  useReducer,
38184
38202
  useRef as useRef71,
38185
38203
  useState as useState44,
@@ -39024,7 +39042,7 @@ function TopologyLens({ iframeRef, activeCompositionPath }) {
39024
39042
  const ownedCallIdRef = useRef71(callId);
39025
39043
  const pendingUnmountDismissRef = useRef71(null);
39026
39044
  ownedCallIdRef.current = callId;
39027
- useLayoutEffect7(() => {
39045
+ useLayoutEffect8(() => {
39028
39046
  if (!callId || !handle) {
39029
39047
  setMeasured(null);
39030
39048
  return;
@@ -49312,7 +49330,7 @@ function ColorGradingSection({
49312
49330
  import { useEffect as useEffect73, useRef as useRef92, useState as useState67 } from "react";
49313
49331
 
49314
49332
  // src/components/editor/propertyPanelColor.tsx
49315
- import { useCallback as useCallback78, useEffect as useEffect68, useLayoutEffect as useLayoutEffect8, useRef as useRef88, useState as useState59 } from "react";
49333
+ import { useCallback as useCallback78, useEffect as useEffect68, useLayoutEffect as useLayoutEffect9, useRef as useRef88, useState as useState59 } from "react";
49316
49334
  import { createPortal as createPortal8 } from "react-dom";
49317
49335
  import { jsx as jsx113, jsxs as jsxs81 } from "react/jsx-runtime";
49318
49336
  var COLOR_PICKER_SIZE = { width: 292, height: 386 };
@@ -49511,7 +49529,7 @@ function ColorField({
49511
49529
  )
49512
49530
  );
49513
49531
  }, []);
49514
- useLayoutEffect8(() => {
49532
+ useLayoutEffect9(() => {
49515
49533
  if (!open) return;
49516
49534
  updatePanelPosition();
49517
49535
  const handlePositionInvalidated = () => updatePanelPosition();
@@ -65569,7 +65587,7 @@ var FileTree = memo38(function FileTree2({
65569
65587
  });
65570
65588
 
65571
65589
  // src/App.tsx
65572
- import { useState as useState148, useCallback as useCallback181, useRef as useRef179, useMemo as useMemo77, useLayoutEffect as useLayoutEffect11 } from "react";
65590
+ import { useState as useState148, useCallback as useCallback181, useRef as useRef179, useMemo as useMemo77, useLayoutEffect as useLayoutEffect12 } from "react";
65573
65591
 
65574
65592
  // src/hooks/useRightPanelIntents.ts
65575
65593
  import { useCallback as useCallback88, useEffect as useEffect97, useRef as useRef114 } from "react";
@@ -82227,7 +82245,7 @@ import { useState as useState124, useRef as useRef162, useEffect as useEffect126
82227
82245
  import { classifyWebAudioMediaRoute } from "@hyperframes/core/runtime/web-audio-route";
82228
82246
 
82229
82247
  // src/components/sidebar/AssetContextMenu.tsx
82230
- import { useCallback as useCallback152, useEffect as useEffect125, useLayoutEffect as useLayoutEffect9, useRef as useRef161, useState as useState123 } from "react";
82248
+ import { useCallback as useCallback152, useEffect as useEffect125, useLayoutEffect as useLayoutEffect10, useRef as useRef161, useState as useState123 } from "react";
82231
82249
  import { Fragment as Fragment54, jsx as jsx193, jsxs as jsxs153 } from "react/jsx-runtime";
82232
82250
  function isValidAssetName(name) {
82233
82251
  return name.length > 0 && !/[/\\]/.test(name) && !name.includes("..");
@@ -82247,7 +82265,7 @@ function ContextMenu3({
82247
82265
  const [mode, setMode] = useState123("menu");
82248
82266
  const [renameDraft, setRenameDraft] = useState123(() => filename(asset));
82249
82267
  const [renameError, setRenameError] = useState123(null);
82250
- useLayoutEffect9(() => {
82268
+ useLayoutEffect10(() => {
82251
82269
  const el = menuRef.current;
82252
82270
  if (!el) return;
82253
82271
  const rect = el.getBoundingClientRect();
@@ -85935,7 +85953,7 @@ function ParamControl({
85935
85953
  import { memo as memo51 } from "react";
85936
85954
 
85937
85955
  // src/components/renders/RenderQueue.tsx
85938
- import { memo as memo50, useState as useState139, useRef as useRef173, useEffect as useEffect138, useLayoutEffect as useLayoutEffect10, useId as useId5 } from "react";
85956
+ import { memo as memo50, useState as useState139, useRef as useRef173, useEffect as useEffect138, useLayoutEffect as useLayoutEffect11, useId as useId5 } from "react";
85939
85957
  import { createPortal as createPortal10 } from "react-dom";
85940
85958
  import { CANVAS_DIMENSIONS } from "@hyperframes/parsers";
85941
85959
 
@@ -86320,7 +86338,7 @@ function FormatInfoTooltip({ format }) {
86320
86338
  timeoutRef.current = setTimeout(() => setOpen(false), 120);
86321
86339
  };
86322
86340
  useEffect138(() => () => clearTimeout(timeoutRef.current), []);
86323
- useLayoutEffect10(() => {
86341
+ useLayoutEffect11(() => {
86324
86342
  if (!open) return;
86325
86343
  const el = triggerRef.current;
86326
86344
  if (!el) return;
@@ -89369,7 +89387,13 @@ function StudioRightPanels({
89369
89387
 
89370
89388
  // src/components/TimelineToolbar.tsx
89371
89389
  import { useEffect as useEffect144, useRef as useRef176 } from "react";
89372
- import { Image as Image3, Magnet, MagnifyingGlassMinus, MagnifyingGlassPlus } from "@phosphor-icons/react";
89390
+ import {
89391
+ ArrowsOutLineHorizontal,
89392
+ Image as Image3,
89393
+ Magnet,
89394
+ MagnifyingGlassMinus,
89395
+ MagnifyingGlassPlus
89396
+ } from "@phosphor-icons/react";
89373
89397
 
89374
89398
  // src/hooks/useEnableKeyframes.ts
89375
89399
  import { useCallback as useCallback177 } from "react";
@@ -89830,7 +89854,7 @@ function TimelineHistoryButtons(props) {
89830
89854
  }
89831
89855
 
89832
89856
  // src/components/TimelineToolbar.tsx
89833
- import { jsx as jsx225, jsxs as jsxs179 } from "react/jsx-runtime";
89857
+ import { Fragment as Fragment63, jsx as jsx225, jsxs as jsxs179 } from "react/jsx-runtime";
89834
89858
  var NO_KEYFRAME_TOGGLE = {
89835
89859
  state: "none",
89836
89860
  isMotionPath: false,
@@ -89889,7 +89913,8 @@ function TimelineToolbar({
89889
89913
  domEditSession,
89890
89914
  onSplitElement,
89891
89915
  history: history2,
89892
- showAddBeat = true
89916
+ showAddBeat = true,
89917
+ showKeyframes = true
89893
89918
  }) {
89894
89919
  const activeTool = usePlayerStore((s) => s.activeTool);
89895
89920
  const setActiveTool = usePlayerStore((s) => s.setActiveTool);
@@ -89924,9 +89949,12 @@ function TimelineToolbar({
89924
89949
  onToggle: onToggleKeyframe
89925
89950
  } = useKeyframeToggle(domEditSession);
89926
89951
  useKeyframeKeyboard({
89927
- enabled: Boolean(onToggleKeyframe),
89952
+ enabled: showKeyframes && Boolean(onToggleKeyframe),
89928
89953
  onAddKeyframe: onToggleKeyframe
89929
89954
  });
89955
+ useEffect144(() => {
89956
+ if (!showKeyframes) setAutoKeyframeEnabled(false);
89957
+ }, [showKeyframes, setAutoKeyframeEnabled]);
89930
89958
  useEffect144(() => {
89931
89959
  const onKeyDown = (e) => {
89932
89960
  if (e.key !== "n" && e.key !== "N") return;
@@ -90009,61 +90037,63 @@ function TimelineToolbar({
90009
90037
  children: /* @__PURE__ */ jsx225(AudioMetersIcon, { size: 16 })
90010
90038
  }
90011
90039
  ) }),
90012
- /* @__PURE__ */ jsx225(
90013
- Tooltip,
90014
- {
90015
- label: keyframePathEndpoint ? "Motion path endpoints cannot be removed" : !onToggleKeyframe ? "Select an animated element to add keyframes" : keyframeIsMotionPath ? keyframeWillExtend ? "Extend motion path to playhead (K)" : keyframeState === "active" ? "Remove waypoint from motion path (K)" : "Add waypoint to motion path (K)" : keyframeState === "active" ? "Remove keyframe at playhead (K)" : keyframeState === "inactive" ? keyframeWillExtend ? "Add keyframe at playhead, extends animation (K)" : "Add keyframe at playhead (K)" : "Add keyframe (K)",
90016
- children: /* @__PURE__ */ jsx225(
90017
- "button",
90018
- {
90019
- type: "button",
90020
- disabled: !onToggleKeyframe,
90021
- onClick: onToggleKeyframe,
90022
- "aria-label": keyframePathEndpoint ? "Motion path endpoint" : keyframeIsMotionPath ? keyframeState === "active" ? "Remove motion path waypoint" : keyframeWillExtend ? "Extend motion path to playhead" : "Add motion path waypoint" : keyframeState === "active" ? "Remove keyframe at playhead" : "Add keyframe at playhead",
90023
- className: !onToggleKeyframe ? flatDisabled : `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${keyframeState === "active" ? "text-studio-accent" : keyframeState === "inactive" ? "text-neutral-400 hover:text-studio-accent" : "text-neutral-600 hover:text-neutral-400"}`,
90024
- children: /* @__PURE__ */ jsx225("svg", { width: "16", height: "16", viewBox: "0 0 10 10", fill: "currentColor", children: keyframeState === "active" ? /* @__PURE__ */ jsx225("path", { d: "M5 0.5L9.5 5L5 9.5L0.5 5Z" }) : /* @__PURE__ */ jsx225(
90025
- "path",
90026
- {
90027
- d: "M5 1.2L8.8 5L5 8.8L1.2 5Z",
90028
- fill: "none",
90029
- stroke: "currentColor",
90030
- strokeWidth: "1.2"
90031
- }
90032
- ) })
90033
- }
90034
- )
90035
- }
90036
- ),
90037
- /* @__PURE__ */ jsx225(
90038
- Tooltip,
90039
- {
90040
- label: autoKeyframeEnabled ? "Auto-record manual edits as keyframes (click to turn off)" : "Manual edits will not be recorded as keyframes (click to turn on)",
90041
- children: /* @__PURE__ */ jsx225(
90042
- "button",
90043
- {
90044
- type: "button",
90045
- onClick: () => setAutoKeyframeEnabled(!autoKeyframeEnabled),
90046
- "aria-label": "Auto-record manual edits as keyframes",
90047
- "aria-pressed": autoKeyframeEnabled,
90048
- className: `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${autoKeyframeEnabled ? "text-red-400 hover:text-red-300" : "text-neutral-600 hover:text-neutral-400"}`,
90049
- children: /* @__PURE__ */ jsxs179("svg", { width: "16", height: "16", viewBox: "0 0 10 10", fill: "none", children: [
90050
- /* @__PURE__ */ jsx225("path", { d: "M5 0.7L9.3 5L5 9.3L0.7 5Z", stroke: "currentColor", strokeWidth: "1" }),
90051
- /* @__PURE__ */ jsx225(
90052
- "circle",
90040
+ showKeyframes && /* @__PURE__ */ jsxs179(Fragment63, { children: [
90041
+ /* @__PURE__ */ jsx225(
90042
+ Tooltip,
90043
+ {
90044
+ label: keyframePathEndpoint ? "Motion path endpoints cannot be removed" : !onToggleKeyframe ? "Select an animated element to add keyframes" : keyframeIsMotionPath ? keyframeWillExtend ? "Extend motion path to playhead (K)" : keyframeState === "active" ? "Remove waypoint from motion path (K)" : "Add waypoint to motion path (K)" : keyframeState === "active" ? "Remove keyframe at playhead (K)" : keyframeState === "inactive" ? keyframeWillExtend ? "Add keyframe at playhead, extends animation (K)" : "Add keyframe at playhead (K)" : "Add keyframe (K)",
90045
+ children: /* @__PURE__ */ jsx225(
90046
+ "button",
90047
+ {
90048
+ type: "button",
90049
+ disabled: !onToggleKeyframe,
90050
+ onClick: onToggleKeyframe,
90051
+ "aria-label": keyframePathEndpoint ? "Motion path endpoint" : keyframeIsMotionPath ? keyframeState === "active" ? "Remove motion path waypoint" : keyframeWillExtend ? "Extend motion path to playhead" : "Add motion path waypoint" : keyframeState === "active" ? "Remove keyframe at playhead" : "Add keyframe at playhead",
90052
+ className: !onToggleKeyframe ? flatDisabled : `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${keyframeState === "active" ? "text-studio-accent" : keyframeState === "inactive" ? "text-neutral-400 hover:text-studio-accent" : "text-neutral-600 hover:text-neutral-400"}`,
90053
+ children: /* @__PURE__ */ jsx225("svg", { width: "16", height: "16", viewBox: "0 0 10 10", fill: "currentColor", children: keyframeState === "active" ? /* @__PURE__ */ jsx225("path", { d: "M5 0.5L9.5 5L5 9.5L0.5 5Z" }) : /* @__PURE__ */ jsx225(
90054
+ "path",
90053
90055
  {
90054
- cx: "5",
90055
- cy: "5",
90056
- r: "1.8",
90057
- fill: autoKeyframeEnabled ? "currentColor" : "none",
90056
+ d: "M5 1.2L8.8 5L5 8.8L1.2 5Z",
90057
+ fill: "none",
90058
90058
  stroke: "currentColor",
90059
- strokeWidth: "1"
90059
+ strokeWidth: "1.2"
90060
90060
  }
90061
- )
90062
- ] })
90063
- }
90064
- )
90065
- }
90066
- ),
90061
+ ) })
90062
+ }
90063
+ )
90064
+ }
90065
+ ),
90066
+ /* @__PURE__ */ jsx225(
90067
+ Tooltip,
90068
+ {
90069
+ label: autoKeyframeEnabled ? "Auto-record manual edits as keyframes (click to turn off)" : "Manual edits will not be recorded as keyframes (click to turn on)",
90070
+ children: /* @__PURE__ */ jsx225(
90071
+ "button",
90072
+ {
90073
+ type: "button",
90074
+ onClick: () => setAutoKeyframeEnabled(!autoKeyframeEnabled),
90075
+ "aria-label": "Auto-record manual edits as keyframes",
90076
+ "aria-pressed": autoKeyframeEnabled,
90077
+ className: `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${autoKeyframeEnabled ? "text-red-400 hover:text-red-300" : "text-neutral-600 hover:text-neutral-400"}`,
90078
+ children: /* @__PURE__ */ jsxs179("svg", { width: "16", height: "16", viewBox: "0 0 10 10", fill: "none", children: [
90079
+ /* @__PURE__ */ jsx225("path", { d: "M5 0.7L9.3 5L5 9.3L0.7 5Z", stroke: "currentColor", strokeWidth: "1" }),
90080
+ /* @__PURE__ */ jsx225(
90081
+ "circle",
90082
+ {
90083
+ cx: "5",
90084
+ cy: "5",
90085
+ r: "1.8",
90086
+ fill: autoKeyframeEnabled ? "currentColor" : "none",
90087
+ stroke: "currentColor",
90088
+ strokeWidth: "1"
90089
+ }
90090
+ )
90091
+ ] })
90092
+ }
90093
+ )
90094
+ }
90095
+ )
90096
+ ] }),
90067
90097
  onSplitElement && (() => {
90068
90098
  const el = selectedElementId ? elements.find((e) => (e.key ?? e.id) === selectedElementId) : null;
90069
90099
  const splittable = el != null && canSplitElement(el);
@@ -90161,8 +90191,10 @@ function TimelineToolbar({
90161
90191
  {
90162
90192
  type: "button",
90163
90193
  onClick: () => setZoomMode("fit"),
90194
+ "aria-label": "Fit timeline to width",
90195
+ "aria-pressed": zoomMode === "fit",
90164
90196
  className: `h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${zoomMode === "fit" ? "bg-studio-accent/10 text-studio-accent" : "text-neutral-400 hover:bg-white/6 hover:text-neutral-200"}`,
90165
- children: "Fit"
90197
+ children: /* @__PURE__ */ jsx225(ArrowsOutLineHorizontal, { size: 16, "aria-hidden": "true" })
90166
90198
  }
90167
90199
  ) }),
90168
90200
  /* @__PURE__ */ jsx225(Tooltip, { label: "Zoom out", children: /* @__PURE__ */ jsx225(
@@ -90505,7 +90537,7 @@ function useActiveComposition({
90505
90537
  }
90506
90538
 
90507
90539
  // src/App.tsx
90508
- import { Fragment as Fragment63, jsx as jsx227, jsxs as jsxs181 } from "react/jsx-runtime";
90540
+ import { Fragment as Fragment64, jsx as jsx227, jsxs as jsxs181 } from "react/jsx-runtime";
90509
90541
  var getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
90510
90542
  function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
90511
90543
  const { projectId, resolving, waitingForServer } = useServerConnection();
@@ -90801,7 +90833,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
90801
90833
  });
90802
90834
  handleToggleRecordingRef.current = handleToggleRecording;
90803
90835
  const canvasRectRef = useRef179(null);
90804
- useLayoutEffect11(() => {
90836
+ useLayoutEffect12(() => {
90805
90837
  if (gestureState !== "recording" || !previewIframe) {
90806
90838
  canvasRectRef.current = null;
90807
90839
  return;
@@ -90920,7 +90952,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
90920
90952
  {
90921
90953
  readOnlyPreview,
90922
90954
  readOnlyPreviewReason,
90923
- panels: /* @__PURE__ */ jsxs181(Fragment63, { children: [
90955
+ panels: /* @__PURE__ */ jsxs181(Fragment64, { children: [
90924
90956
  /* @__PURE__ */ jsx227(
90925
90957
  StudioLeftPanels,
90926
90958
  {