@qwanyx/carousel 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +2620 -24
  2. package/dist/index.mjs +2602 -24
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -30,13 +30,31 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AnimationEditor: () => AnimationEditor,
33
34
  Carousel: () => Carousel,
35
+ LayerPanel: () => LayerPanel,
36
+ PreviewCanvas: () => PreviewCanvas,
37
+ PropertiesPanel: () => PropertiesPanel,
34
38
  SlideRenderer: () => SlideRenderer,
35
39
  Thumbnails: () => Thumbnails,
40
+ Timeline: () => Timeline,
41
+ createComposition: () => createComposition,
36
42
  createImageSlide: () => createImageSlide,
43
+ createKeyframe: () => createKeyframe,
44
+ createLayerFolder: () => createLayerFolder,
45
+ createLayerItem: () => createLayerItem,
37
46
  createSimpleSlide: () => createSimpleSlide,
38
47
  createSlide: () => createSlide,
39
- useCarousel: () => useCarousel
48
+ findLayerById: () => findLayerById,
49
+ flattenLayers: () => flattenLayers,
50
+ formatTimecode: () => formatTimecode,
51
+ getLayerPropertiesAtTime: () => getLayerPropertiesAtTime,
52
+ interpolateKeyframes: () => interpolateKeyframes,
53
+ parseTimecode: () => parseTimecode,
54
+ useCarousel: () => useCarousel,
55
+ useComposition: () => useComposition,
56
+ useKeyframes: () => useKeyframes,
57
+ usePlayback: () => usePlayback
40
58
  });
41
59
  module.exports = __toCommonJS(index_exports);
42
60
 
@@ -1197,13 +1215,47 @@ var Carousel = (0, import_react4.forwardRef)(
1197
1215
  children: isFullscreen ? "\u22A0" : "\u229E"
1198
1216
  }
1199
1217
  ),
1218
+ currentSlide && (getSlideUrl || extractSlideUrl(currentSlide)) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1219
+ "button",
1220
+ {
1221
+ className: "qc-carousel__download-slide",
1222
+ onClick: () => {
1223
+ const url = getSlideUrl ? getSlideUrl(currentSlide) : extractSlideUrl(currentSlide);
1224
+ if (url) {
1225
+ const link = document.createElement("a");
1226
+ link.href = url;
1227
+ link.download = currentSlide.name || `slide-${currentIndex + 1}`;
1228
+ link.target = "_blank";
1229
+ document.body.appendChild(link);
1230
+ link.click();
1231
+ document.body.removeChild(link);
1232
+ }
1233
+ },
1234
+ "aria-label": "Download",
1235
+ title: "Download",
1236
+ style: {
1237
+ width: "40px",
1238
+ height: "40px",
1239
+ borderRadius: "8px",
1240
+ border: "none",
1241
+ backgroundColor: "rgba(0,0,0,0.5)",
1242
+ color: "white",
1243
+ cursor: "pointer",
1244
+ display: "flex",
1245
+ alignItems: "center",
1246
+ justifyContent: "center",
1247
+ fontSize: "18px"
1248
+ },
1249
+ children: "\u2B07"
1250
+ }
1251
+ ),
1200
1252
  currentSlide && (getSlideUrl || extractSlideUrl(currentSlide)) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1201
1253
  "button",
1202
1254
  {
1203
1255
  className: "qc-carousel__copy-slide",
1204
1256
  onClick: () => handleCopySlide(currentSlide),
1205
- "aria-label": "Copy image URL",
1206
- title: "Copy image URL",
1257
+ "aria-label": "Copy URL",
1258
+ title: "Copy URL",
1207
1259
  style: {
1208
1260
  width: "40px",
1209
1261
  height: "40px",
@@ -1372,6 +1424,9 @@ var Carousel = (0, import_react4.forwardRef)(
1372
1424
  );
1373
1425
  Carousel.displayName = "Carousel";
1374
1426
 
1427
+ // src/components/animation/AnimationEditor.tsx
1428
+ var import_react18 = __toESM(require("react"));
1429
+
1375
1430
  // src/types.ts
1376
1431
  function createSimpleSlide(id, objects, options) {
1377
1432
  return {
@@ -1412,14 +1467,2537 @@ function createImageSlide(id, src, options) {
1412
1467
  }
1413
1468
  return createSimpleSlide(id, objects, { background: options?.background });
1414
1469
  }
1470
+ function formatTimecode(ms) {
1471
+ const minutes = Math.floor(ms / 6e4);
1472
+ const seconds = Math.floor(ms % 6e4 / 1e3);
1473
+ const millis = ms % 1e3;
1474
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${millis.toString().padStart(3, "0")}`;
1475
+ }
1476
+ function parseTimecode(str) {
1477
+ const parts = str.split(":");
1478
+ if (parts.length !== 3) return 0;
1479
+ const minutes = parseInt(parts[0], 10) || 0;
1480
+ const seconds = parseInt(parts[1], 10) || 0;
1481
+ const millis = parseInt(parts[2], 10) || 0;
1482
+ return minutes * 6e4 + seconds * 1e3 + millis;
1483
+ }
1484
+ function createComposition(id, name, options) {
1485
+ return {
1486
+ id,
1487
+ name,
1488
+ width: 1920,
1489
+ height: 1080,
1490
+ aspectRatio: "16/9",
1491
+ duration: 3e4,
1492
+ // 30 seconds default
1493
+ layers: [],
1494
+ ...options
1495
+ };
1496
+ }
1497
+ function createLayerFolder(id, name, children = []) {
1498
+ return {
1499
+ id,
1500
+ name,
1501
+ type: "folder",
1502
+ visible: true,
1503
+ locked: false,
1504
+ opacity: 1,
1505
+ children
1506
+ };
1507
+ }
1508
+ function createLayerItem(id, name, object) {
1509
+ return {
1510
+ id,
1511
+ name,
1512
+ type: "item",
1513
+ visible: true,
1514
+ locked: false,
1515
+ opacity: 1,
1516
+ object
1517
+ };
1518
+ }
1519
+ function createKeyframe(time, properties, easing = "ease-out") {
1520
+ return {
1521
+ id: `kf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
1522
+ time,
1523
+ properties,
1524
+ easing
1525
+ };
1526
+ }
1527
+
1528
+ // src/components/animation/hooks/useComposition.ts
1529
+ var import_react5 = require("react");
1530
+ function createInitialState(composition) {
1531
+ return {
1532
+ composition,
1533
+ selection: {
1534
+ layerIds: [],
1535
+ keyframeIds: []
1536
+ },
1537
+ playback: {
1538
+ isPlaying: false,
1539
+ currentTime: 0,
1540
+ playbackRate: 1
1541
+ },
1542
+ timeline: {
1543
+ zoom: 50,
1544
+ // 50px per second
1545
+ scrollX: 0,
1546
+ scrollY: 0,
1547
+ snapToGrid: true,
1548
+ gridSize: 100
1549
+ // Snap to 100ms
1550
+ },
1551
+ tool: "select",
1552
+ panels: {
1553
+ layers: true,
1554
+ properties: true,
1555
+ timeline: true
1556
+ },
1557
+ isDirty: false
1558
+ };
1559
+ }
1560
+ function findLayerById(layers, id) {
1561
+ for (const layer of layers) {
1562
+ if (layer.id === id) return layer;
1563
+ if (layer.type === "folder") {
1564
+ const found = findLayerById(layer.children, id);
1565
+ if (found) return found;
1566
+ }
1567
+ }
1568
+ return null;
1569
+ }
1570
+ function updateLayerInTree(layers, layerId, updater) {
1571
+ return layers.map((layer) => {
1572
+ if (layer.id === layerId) {
1573
+ return updater(layer);
1574
+ }
1575
+ if (layer.type === "folder") {
1576
+ return {
1577
+ ...layer,
1578
+ children: updateLayerInTree(layer.children, layerId, updater)
1579
+ };
1580
+ }
1581
+ return layer;
1582
+ });
1583
+ }
1584
+ function removeLayerFromTree(layers, layerId) {
1585
+ return layers.filter((layer) => layer.id !== layerId).map((layer) => {
1586
+ if (layer.type === "folder") {
1587
+ return {
1588
+ ...layer,
1589
+ children: removeLayerFromTree(layer.children, layerId)
1590
+ };
1591
+ }
1592
+ return layer;
1593
+ });
1594
+ }
1595
+ function addLayerToTree(layers, newLayer, parentId, index) {
1596
+ if (!parentId) {
1597
+ const idx = index ?? layers.length;
1598
+ return [...layers.slice(0, idx), newLayer, ...layers.slice(idx)];
1599
+ }
1600
+ return layers.map((layer) => {
1601
+ if (layer.id === parentId && layer.type === "folder") {
1602
+ const idx = index ?? layer.children.length;
1603
+ return {
1604
+ ...layer,
1605
+ children: [
1606
+ ...layer.children.slice(0, idx),
1607
+ newLayer,
1608
+ ...layer.children.slice(idx)
1609
+ ]
1610
+ };
1611
+ }
1612
+ if (layer.type === "folder") {
1613
+ return {
1614
+ ...layer,
1615
+ children: addLayerToTree(layer.children, newLayer, parentId, index)
1616
+ };
1617
+ }
1618
+ return layer;
1619
+ });
1620
+ }
1621
+ function flattenLayers(layers, collapsedIds = /* @__PURE__ */ new Set()) {
1622
+ const result = [];
1623
+ for (const layer of layers) {
1624
+ result.push(layer);
1625
+ if (layer.type === "folder" && !collapsedIds.has(layer.id) && !layer.collapsed) {
1626
+ result.push(...flattenLayers(layer.children, collapsedIds));
1627
+ }
1628
+ }
1629
+ return result;
1630
+ }
1631
+ function editorReducer(state, action) {
1632
+ switch (action.type) {
1633
+ // Composition
1634
+ case "SET_COMPOSITION":
1635
+ return { ...state, composition: action.payload, isDirty: false };
1636
+ case "UPDATE_COMPOSITION":
1637
+ return {
1638
+ ...state,
1639
+ composition: { ...state.composition, ...action.payload },
1640
+ isDirty: true
1641
+ };
1642
+ // Layers
1643
+ case "ADD_LAYER":
1644
+ return {
1645
+ ...state,
1646
+ composition: {
1647
+ ...state.composition,
1648
+ layers: addLayerToTree(
1649
+ state.composition.layers,
1650
+ action.payload.layer,
1651
+ action.payload.parentId,
1652
+ action.payload.index
1653
+ )
1654
+ },
1655
+ isDirty: true
1656
+ };
1657
+ case "REMOVE_LAYER":
1658
+ return {
1659
+ ...state,
1660
+ composition: {
1661
+ ...state.composition,
1662
+ layers: removeLayerFromTree(state.composition.layers, action.payload.layerId)
1663
+ },
1664
+ selection: {
1665
+ ...state.selection,
1666
+ layerIds: state.selection.layerIds.filter((id) => id !== action.payload.layerId)
1667
+ },
1668
+ isDirty: true
1669
+ };
1670
+ case "UPDATE_LAYER":
1671
+ return {
1672
+ ...state,
1673
+ composition: {
1674
+ ...state.composition,
1675
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1676
+ ...layer,
1677
+ ...action.payload.updates
1678
+ }))
1679
+ },
1680
+ isDirty: true
1681
+ };
1682
+ case "TOGGLE_LAYER_VISIBILITY":
1683
+ return {
1684
+ ...state,
1685
+ composition: {
1686
+ ...state.composition,
1687
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1688
+ ...layer,
1689
+ visible: !layer.visible
1690
+ }))
1691
+ },
1692
+ isDirty: true
1693
+ };
1694
+ case "TOGGLE_LAYER_LOCK":
1695
+ return {
1696
+ ...state,
1697
+ composition: {
1698
+ ...state.composition,
1699
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1700
+ ...layer,
1701
+ locked: !layer.locked
1702
+ }))
1703
+ },
1704
+ isDirty: true
1705
+ };
1706
+ case "TOGGLE_FOLDER_COLLAPSED":
1707
+ return {
1708
+ ...state,
1709
+ composition: {
1710
+ ...state.composition,
1711
+ layers: updateLayerInTree(state.composition.layers, action.payload.folderId, (layer) => {
1712
+ if (layer.type !== "folder") return layer;
1713
+ return { ...layer, collapsed: !layer.collapsed };
1714
+ })
1715
+ }
1716
+ };
1717
+ // Keyframes
1718
+ case "ADD_KEYFRAME":
1719
+ return {
1720
+ ...state,
1721
+ composition: {
1722
+ ...state.composition,
1723
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1724
+ ...layer,
1725
+ keyframes: [...layer.keyframes || [], action.payload.keyframe].sort((a, b) => a.time - b.time)
1726
+ }))
1727
+ },
1728
+ isDirty: true
1729
+ };
1730
+ case "REMOVE_KEYFRAME":
1731
+ return {
1732
+ ...state,
1733
+ composition: {
1734
+ ...state.composition,
1735
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1736
+ ...layer,
1737
+ keyframes: (layer.keyframes || []).filter((kf) => kf.id !== action.payload.keyframeId)
1738
+ }))
1739
+ },
1740
+ selection: {
1741
+ ...state.selection,
1742
+ keyframeIds: state.selection.keyframeIds.filter((id) => id !== action.payload.keyframeId)
1743
+ },
1744
+ isDirty: true
1745
+ };
1746
+ case "UPDATE_KEYFRAME":
1747
+ return {
1748
+ ...state,
1749
+ composition: {
1750
+ ...state.composition,
1751
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1752
+ ...layer,
1753
+ keyframes: (layer.keyframes || []).map(
1754
+ (kf) => kf.id === action.payload.keyframeId ? { ...kf, ...action.payload.updates } : kf
1755
+ )
1756
+ }))
1757
+ },
1758
+ isDirty: true
1759
+ };
1760
+ case "MOVE_KEYFRAME":
1761
+ return {
1762
+ ...state,
1763
+ composition: {
1764
+ ...state.composition,
1765
+ layers: updateLayerInTree(state.composition.layers, action.payload.layerId, (layer) => ({
1766
+ ...layer,
1767
+ keyframes: (layer.keyframes || []).map((kf) => kf.id === action.payload.keyframeId ? { ...kf, time: action.payload.newTime } : kf).sort((a, b) => a.time - b.time)
1768
+ }))
1769
+ },
1770
+ isDirty: true
1771
+ };
1772
+ // Selection
1773
+ case "SELECT_LAYERS":
1774
+ return {
1775
+ ...state,
1776
+ selection: {
1777
+ ...state.selection,
1778
+ layerIds: action.payload.additive ? [.../* @__PURE__ */ new Set([...state.selection.layerIds, ...action.payload.layerIds])] : action.payload.layerIds
1779
+ }
1780
+ };
1781
+ case "SELECT_KEYFRAMES":
1782
+ return {
1783
+ ...state,
1784
+ selection: {
1785
+ ...state.selection,
1786
+ keyframeIds: action.payload.additive ? [.../* @__PURE__ */ new Set([...state.selection.keyframeIds, ...action.payload.keyframeIds])] : action.payload.keyframeIds
1787
+ }
1788
+ };
1789
+ case "CLEAR_SELECTION":
1790
+ return {
1791
+ ...state,
1792
+ selection: { layerIds: [], keyframeIds: [] }
1793
+ };
1794
+ // Playback
1795
+ case "PLAY":
1796
+ return { ...state, playback: { ...state.playback, isPlaying: true } };
1797
+ case "PAUSE":
1798
+ return { ...state, playback: { ...state.playback, isPlaying: false } };
1799
+ case "STOP":
1800
+ return { ...state, playback: { ...state.playback, isPlaying: false, currentTime: 0 } };
1801
+ case "SEEK":
1802
+ return { ...state, playback: { ...state.playback, currentTime: action.payload.time } };
1803
+ case "SET_PLAYBACK_RATE":
1804
+ return { ...state, playback: { ...state.playback, playbackRate: action.payload.rate } };
1805
+ // Timeline view
1806
+ case "SET_TIMELINE_ZOOM":
1807
+ return { ...state, timeline: { ...state.timeline, zoom: action.payload.zoom } };
1808
+ case "SET_TIMELINE_SCROLL":
1809
+ return {
1810
+ ...state,
1811
+ timeline: {
1812
+ ...state.timeline,
1813
+ scrollX: action.payload.scrollX ?? state.timeline.scrollX,
1814
+ scrollY: action.payload.scrollY ?? state.timeline.scrollY
1815
+ }
1816
+ };
1817
+ case "TOGGLE_SNAP_TO_GRID":
1818
+ return { ...state, timeline: { ...state.timeline, snapToGrid: !state.timeline.snapToGrid } };
1819
+ case "SET_GRID_SIZE":
1820
+ return { ...state, timeline: { ...state.timeline, gridSize: action.payload.size } };
1821
+ // Tool
1822
+ case "SET_TOOL":
1823
+ return { ...state, tool: action.payload.tool };
1824
+ // Panels
1825
+ case "TOGGLE_PANEL":
1826
+ return {
1827
+ ...state,
1828
+ panels: { ...state.panels, [action.payload.panel]: !state.panels[action.payload.panel] }
1829
+ };
1830
+ // History (placeholder - would need separate history implementation)
1831
+ case "UNDO":
1832
+ case "REDO":
1833
+ return state;
1834
+ case "MARK_SAVED":
1835
+ return { ...state, isDirty: false };
1836
+ default:
1837
+ return state;
1838
+ }
1839
+ }
1840
+ function useComposition(initialComposition) {
1841
+ const [state, dispatch] = (0, import_react5.useReducer)(editorReducer, initialComposition, createInitialState);
1842
+ const selectedLayers = (0, import_react5.useMemo)(() => {
1843
+ return state.selection.layerIds.map((id) => findLayerById(state.composition.layers, id)).filter((layer) => layer !== null);
1844
+ }, [state.composition.layers, state.selection.layerIds]);
1845
+ const flattenedLayers = (0, import_react5.useMemo)(() => {
1846
+ const collapsedIds = /* @__PURE__ */ new Set();
1847
+ const collectCollapsed = (layers) => {
1848
+ for (const layer of layers) {
1849
+ if (layer.type === "folder") {
1850
+ if (layer.collapsed) collapsedIds.add(layer.id);
1851
+ collectCollapsed(layer.children);
1852
+ }
1853
+ }
1854
+ };
1855
+ collectCollapsed(state.composition.layers);
1856
+ return flattenLayers(state.composition.layers, collapsedIds);
1857
+ }, [state.composition.layers]);
1858
+ return {
1859
+ state,
1860
+ dispatch,
1861
+ selectedLayers,
1862
+ flattenedLayers
1863
+ };
1864
+ }
1865
+
1866
+ // src/components/animation/hooks/usePlayback.ts
1867
+ var import_react6 = require("react");
1868
+ function usePlayback({
1869
+ duration,
1870
+ currentTime,
1871
+ isPlaying,
1872
+ playbackRate,
1873
+ audioTracks,
1874
+ onTimeUpdate,
1875
+ onPlaybackEnd
1876
+ }) {
1877
+ const animationFrameRef = (0, import_react6.useRef)(null);
1878
+ const lastTimeRef = (0, import_react6.useRef)(0);
1879
+ const audioElementsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
1880
+ (0, import_react6.useEffect)(() => {
1881
+ if (!audioTracks) return;
1882
+ const audioElements = /* @__PURE__ */ new Map();
1883
+ for (const track of audioTracks) {
1884
+ const audio = new Audio(track.src);
1885
+ audio.volume = track.volume ?? 1;
1886
+ audio.loop = track.loop ?? false;
1887
+ audio.muted = track.muted ?? false;
1888
+ audioElements.set(track.id, audio);
1889
+ }
1890
+ audioElementsRef.current = audioElements;
1891
+ return () => {
1892
+ for (const audio of audioElements.values()) {
1893
+ audio.pause();
1894
+ audio.src = "";
1895
+ }
1896
+ };
1897
+ }, [audioTracks]);
1898
+ const syncAudio = (0, import_react6.useCallback)(
1899
+ (time, playing) => {
1900
+ if (!audioTracks) return;
1901
+ for (const track of audioTracks) {
1902
+ const audio = audioElementsRef.current.get(track.id);
1903
+ if (!audio) continue;
1904
+ const trackStart = track.offset ?? 0;
1905
+ const trackEnd = trackStart + (audio.duration * 1e3 || Infinity);
1906
+ const audioStart = track.startTime ?? 0;
1907
+ if (time >= trackStart && time < trackEnd) {
1908
+ const audioTime = (time - trackStart + audioStart) / 1e3;
1909
+ if (Math.abs(audio.currentTime - audioTime) > 0.1) {
1910
+ audio.currentTime = audioTime;
1911
+ }
1912
+ if (playing && audio.paused) {
1913
+ audio.playbackRate = playbackRate;
1914
+ audio.play().catch(() => {
1915
+ });
1916
+ } else if (!playing && !audio.paused) {
1917
+ audio.pause();
1918
+ }
1919
+ } else {
1920
+ if (!audio.paused) {
1921
+ audio.pause();
1922
+ }
1923
+ }
1924
+ }
1925
+ },
1926
+ [audioTracks, playbackRate]
1927
+ );
1928
+ (0, import_react6.useEffect)(() => {
1929
+ if (!isPlaying) {
1930
+ if (animationFrameRef.current) {
1931
+ cancelAnimationFrame(animationFrameRef.current);
1932
+ animationFrameRef.current = null;
1933
+ }
1934
+ syncAudio(currentTime, false);
1935
+ return;
1936
+ }
1937
+ lastTimeRef.current = performance.now();
1938
+ const tick = (now) => {
1939
+ const deltaMs = (now - lastTimeRef.current) * playbackRate;
1940
+ lastTimeRef.current = now;
1941
+ const newTime = Math.min(currentTime + deltaMs, duration);
1942
+ if (newTime >= duration) {
1943
+ onTimeUpdate(duration);
1944
+ onPlaybackEnd?.();
1945
+ return;
1946
+ }
1947
+ onTimeUpdate(newTime);
1948
+ syncAudio(newTime, true);
1949
+ animationFrameRef.current = requestAnimationFrame(tick);
1950
+ };
1951
+ animationFrameRef.current = requestAnimationFrame(tick);
1952
+ return () => {
1953
+ if (animationFrameRef.current) {
1954
+ cancelAnimationFrame(animationFrameRef.current);
1955
+ }
1956
+ };
1957
+ }, [isPlaying, currentTime, duration, playbackRate, onTimeUpdate, onPlaybackEnd, syncAudio]);
1958
+ const seek = (0, import_react6.useCallback)(
1959
+ (time) => {
1960
+ onTimeUpdate(Math.max(0, Math.min(time, duration)));
1961
+ syncAudio(time, isPlaying);
1962
+ },
1963
+ [duration, isPlaying, onTimeUpdate, syncAudio]
1964
+ );
1965
+ const getAudioWaveform = (0, import_react6.useCallback)(async (trackId) => {
1966
+ const audio = audioElementsRef.current.get(trackId);
1967
+ if (!audio) return null;
1968
+ try {
1969
+ const response = await fetch(audio.src);
1970
+ const arrayBuffer = await response.arrayBuffer();
1971
+ const audioContext = new AudioContext();
1972
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
1973
+ const channelData = audioBuffer.getChannelData(0);
1974
+ const samples = 1e3;
1975
+ const blockSize = Math.floor(channelData.length / samples);
1976
+ const waveform = [];
1977
+ for (let i = 0; i < samples; i++) {
1978
+ let sum = 0;
1979
+ for (let j = 0; j < blockSize; j++) {
1980
+ sum += Math.abs(channelData[i * blockSize + j]);
1981
+ }
1982
+ waveform.push(sum / blockSize);
1983
+ }
1984
+ const max = Math.max(...waveform);
1985
+ return waveform.map((v) => v / max);
1986
+ } catch {
1987
+ return null;
1988
+ }
1989
+ }, []);
1990
+ return {
1991
+ seek,
1992
+ getAudioWaveform,
1993
+ audioElements: audioElementsRef.current
1994
+ };
1995
+ }
1996
+
1997
+ // src/components/animation/layers/LayerPanel.tsx
1998
+ var import_react9 = __toESM(require("react"));
1999
+
2000
+ // src/components/animation/layers/LayerItem.tsx
2001
+ var import_react7 = __toESM(require("react"));
2002
+ var import_jsx_runtime4 = require("react/jsx-runtime");
2003
+ function LayerItemRow({
2004
+ layer,
2005
+ depth,
2006
+ isSelected,
2007
+ onSelect,
2008
+ onToggleVisibility,
2009
+ onToggleLock,
2010
+ onRename,
2011
+ onDelete
2012
+ }) {
2013
+ const [isEditing, setIsEditing] = import_react7.default.useState(false);
2014
+ const [editName, setEditName] = import_react7.default.useState(layer.name);
2015
+ const inputRef = import_react7.default.useRef(null);
2016
+ import_react7.default.useEffect(() => {
2017
+ if (isEditing && inputRef.current) {
2018
+ inputRef.current.focus();
2019
+ inputRef.current.select();
2020
+ }
2021
+ }, [isEditing]);
2022
+ const handleDoubleClick = () => {
2023
+ if (!layer.locked) {
2024
+ setIsEditing(true);
2025
+ setEditName(layer.name);
2026
+ }
2027
+ };
2028
+ const handleNameSubmit = () => {
2029
+ if (editName.trim() && editName !== layer.name) {
2030
+ onRename(layer.id, editName.trim());
2031
+ }
2032
+ setIsEditing(false);
2033
+ };
2034
+ const handleKeyDown = (e) => {
2035
+ if (e.key === "Enter") {
2036
+ handleNameSubmit();
2037
+ } else if (e.key === "Escape") {
2038
+ setIsEditing(false);
2039
+ setEditName(layer.name);
2040
+ }
2041
+ };
2042
+ const getObjectIcon = () => {
2043
+ switch (layer.object.type) {
2044
+ case "image":
2045
+ return "image";
2046
+ case "text":
2047
+ return "text_fields";
2048
+ case "video":
2049
+ return "videocam";
2050
+ case "audio":
2051
+ return "audiotrack";
2052
+ case "shape":
2053
+ return "category";
2054
+ case "component":
2055
+ return "widgets";
2056
+ case "group":
2057
+ return "layers";
2058
+ default:
2059
+ return "lens";
2060
+ }
2061
+ };
2062
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2063
+ "div",
2064
+ {
2065
+ className: `
2066
+ flex items-center h-8 px-2 cursor-pointer select-none
2067
+ ${isSelected ? "bg-blue-500/20" : "hover:bg-neutral-100"}
2068
+ ${!layer.visible ? "opacity-50" : ""}
2069
+ `,
2070
+ style: { paddingLeft: `${depth * 16 + 8}px` },
2071
+ onClick: (e) => onSelect(layer.id, e.shiftKey || e.ctrlKey || e.metaKey),
2072
+ onDoubleClick: handleDoubleClick,
2073
+ children: [
2074
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-400 mr-2", style: { fontSize: 16 }, children: getObjectIcon() }),
2075
+ isEditing ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2076
+ "input",
2077
+ {
2078
+ ref: inputRef,
2079
+ type: "text",
2080
+ value: editName,
2081
+ onChange: (e) => setEditName(e.target.value),
2082
+ onBlur: handleNameSubmit,
2083
+ onKeyDown: handleKeyDown,
2084
+ className: "flex-1 px-1 text-sm bg-white border border-blue-500 rounded outline-none",
2085
+ onClick: (e) => e.stopPropagation()
2086
+ }
2087
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex-1 text-sm text-neutral-700 truncate", children: layer.name }),
2088
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [
2089
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2090
+ "button",
2091
+ {
2092
+ onClick: (e) => {
2093
+ e.stopPropagation();
2094
+ onToggleVisibility(layer.id);
2095
+ },
2096
+ className: "p-0.5 hover:bg-neutral-200 rounded",
2097
+ title: layer.visible ? "Hide" : "Show",
2098
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-400", style: { fontSize: 14 }, children: layer.visible ? "visibility" : "visibility_off" })
2099
+ }
2100
+ ),
2101
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2102
+ "button",
2103
+ {
2104
+ onClick: (e) => {
2105
+ e.stopPropagation();
2106
+ onToggleLock(layer.id);
2107
+ },
2108
+ className: "p-0.5 hover:bg-neutral-200 rounded",
2109
+ title: layer.locked ? "Unlock" : "Lock",
2110
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-400", style: { fontSize: 14 }, children: layer.locked ? "lock" : "lock_open" })
2111
+ }
2112
+ ),
2113
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2114
+ "button",
2115
+ {
2116
+ onClick: (e) => {
2117
+ e.stopPropagation();
2118
+ onDelete(layer.id);
2119
+ },
2120
+ className: "p-0.5 hover:bg-red-100 rounded",
2121
+ title: "Delete",
2122
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-400 hover:text-red-500", style: { fontSize: 14 }, children: "delete" })
2123
+ }
2124
+ )
2125
+ ] })
2126
+ ]
2127
+ }
2128
+ );
2129
+ }
2130
+
2131
+ // src/components/animation/layers/LayerFolder.tsx
2132
+ var import_react8 = __toESM(require("react"));
2133
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2134
+ function LayerFolderRow({
2135
+ folder,
2136
+ depth,
2137
+ selectedIds,
2138
+ onSelect,
2139
+ onToggleVisibility,
2140
+ onToggleLock,
2141
+ onToggleCollapsed,
2142
+ onRename,
2143
+ onDelete,
2144
+ renderLayer
2145
+ }) {
2146
+ const [isEditing, setIsEditing] = import_react8.default.useState(false);
2147
+ const [editName, setEditName] = import_react8.default.useState(folder.name);
2148
+ const inputRef = import_react8.default.useRef(null);
2149
+ const isSelected = selectedIds.includes(folder.id);
2150
+ import_react8.default.useEffect(() => {
2151
+ if (isEditing && inputRef.current) {
2152
+ inputRef.current.focus();
2153
+ inputRef.current.select();
2154
+ }
2155
+ }, [isEditing]);
2156
+ const handleDoubleClick = (e) => {
2157
+ e.stopPropagation();
2158
+ if (!folder.locked) {
2159
+ setIsEditing(true);
2160
+ setEditName(folder.name);
2161
+ }
2162
+ };
2163
+ const handleNameSubmit = () => {
2164
+ if (editName.trim() && editName !== folder.name) {
2165
+ onRename(folder.id, editName.trim());
2166
+ }
2167
+ setIsEditing(false);
2168
+ };
2169
+ const handleKeyDown = (e) => {
2170
+ if (e.key === "Enter") {
2171
+ handleNameSubmit();
2172
+ } else if (e.key === "Escape") {
2173
+ setIsEditing(false);
2174
+ setEditName(folder.name);
2175
+ }
2176
+ };
2177
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "group", children: [
2178
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2179
+ "div",
2180
+ {
2181
+ className: `
2182
+ flex items-center h-8 px-2 cursor-pointer select-none
2183
+ ${isSelected ? "bg-blue-500/20" : "hover:bg-neutral-100"}
2184
+ ${!folder.visible ? "opacity-50" : ""}
2185
+ `,
2186
+ style: { paddingLeft: `${depth * 16 + 8}px` },
2187
+ onClick: (e) => onSelect(folder.id, e.shiftKey || e.ctrlKey || e.metaKey),
2188
+ onDoubleClick: handleDoubleClick,
2189
+ children: [
2190
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2191
+ "button",
2192
+ {
2193
+ onClick: (e) => {
2194
+ e.stopPropagation();
2195
+ onToggleCollapsed(folder.id);
2196
+ },
2197
+ className: "p-0.5 hover:bg-neutral-200 rounded mr-1",
2198
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2199
+ "span",
2200
+ {
2201
+ className: "material-icons text-neutral-400 transition-transform",
2202
+ style: {
2203
+ fontSize: 14,
2204
+ transform: folder.collapsed ? "rotate(-90deg)" : "rotate(0deg)"
2205
+ },
2206
+ children: "expand_more"
2207
+ }
2208
+ )
2209
+ }
2210
+ ),
2211
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-amber-500 mr-2", style: { fontSize: 16 }, children: folder.collapsed ? "folder" : "folder_open" }),
2212
+ isEditing ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2213
+ "input",
2214
+ {
2215
+ ref: inputRef,
2216
+ type: "text",
2217
+ value: editName,
2218
+ onChange: (e) => setEditName(e.target.value),
2219
+ onBlur: handleNameSubmit,
2220
+ onKeyDown: handleKeyDown,
2221
+ className: "flex-1 px-1 text-sm bg-white border border-blue-500 rounded outline-none",
2222
+ onClick: (e) => e.stopPropagation()
2223
+ }
2224
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "flex-1 text-sm font-medium text-neutral-800 truncate", children: folder.name }),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "text-xs text-neutral-400 mr-2", children: [
2226
+ "(",
2227
+ folder.children.length,
2228
+ ")"
2229
+ ] }),
2230
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [
2231
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2232
+ "button",
2233
+ {
2234
+ onClick: (e) => {
2235
+ e.stopPropagation();
2236
+ onToggleVisibility(folder.id);
2237
+ },
2238
+ className: "p-0.5 hover:bg-neutral-200 rounded",
2239
+ title: folder.visible ? "Hide" : "Show",
2240
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-neutral-400", style: { fontSize: 14 }, children: folder.visible ? "visibility" : "visibility_off" })
2241
+ }
2242
+ ),
2243
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2244
+ "button",
2245
+ {
2246
+ onClick: (e) => {
2247
+ e.stopPropagation();
2248
+ onToggleLock(folder.id);
2249
+ },
2250
+ className: "p-0.5 hover:bg-neutral-200 rounded",
2251
+ title: folder.locked ? "Unlock" : "Lock",
2252
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-neutral-400", style: { fontSize: 14 }, children: folder.locked ? "lock" : "lock_open" })
2253
+ }
2254
+ ),
2255
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2256
+ "button",
2257
+ {
2258
+ onClick: (e) => {
2259
+ e.stopPropagation();
2260
+ onDelete(folder.id);
2261
+ },
2262
+ className: "p-0.5 hover:bg-red-100 rounded",
2263
+ title: "Delete folder",
2264
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-neutral-400 hover:text-red-500", style: { fontSize: 14 }, children: "delete" })
2265
+ }
2266
+ )
2267
+ ] })
2268
+ ]
2269
+ }
2270
+ ),
2271
+ !folder.collapsed && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { children: folder.children.map((child) => renderLayer(child, depth + 1)) })
2272
+ ] });
2273
+ }
2274
+
2275
+ // src/components/animation/layers/LayerPanel.tsx
2276
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2277
+ function LayerPanel({
2278
+ layers,
2279
+ selectedIds,
2280
+ onSelect,
2281
+ onToggleVisibility,
2282
+ onToggleLock,
2283
+ onToggleCollapsed,
2284
+ onAddFolder,
2285
+ onAddItem,
2286
+ onDelete,
2287
+ onRename
2288
+ }) {
2289
+ const [showAddMenu, setShowAddMenu] = import_react9.default.useState(false);
2290
+ const renderLayer = (layer, depth) => {
2291
+ if (layer.type === "folder") {
2292
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2293
+ LayerFolderRow,
2294
+ {
2295
+ folder: layer,
2296
+ depth,
2297
+ selectedIds,
2298
+ onSelect,
2299
+ onToggleVisibility,
2300
+ onToggleLock,
2301
+ onToggleCollapsed,
2302
+ onRename,
2303
+ onDelete,
2304
+ renderLayer
2305
+ },
2306
+ layer.id
2307
+ );
2308
+ }
2309
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2310
+ LayerItemRow,
2311
+ {
2312
+ layer,
2313
+ depth,
2314
+ isSelected: selectedIds.includes(layer.id),
2315
+ onSelect,
2316
+ onToggleVisibility,
2317
+ onToggleLock,
2318
+ onRename,
2319
+ onDelete
2320
+ },
2321
+ layer.id
2322
+ );
2323
+ };
2324
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col h-full bg-white border-r border-neutral-200", children: [
2325
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-neutral-200", children: [
2326
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-sm font-semibold text-neutral-700", children: "Layers" }),
2327
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "relative", children: [
2328
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2329
+ "button",
2330
+ {
2331
+ onClick: () => setShowAddMenu(!showAddMenu),
2332
+ className: "p-1 hover:bg-neutral-100 rounded",
2333
+ title: "Add layer",
2334
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-neutral-500", style: { fontSize: 18 }, children: "add" })
2335
+ }
2336
+ ),
2337
+ showAddMenu && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2338
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2339
+ "div",
2340
+ {
2341
+ className: "fixed inset-0 z-10",
2342
+ onClick: () => setShowAddMenu(false)
2343
+ }
2344
+ ),
2345
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-neutral-200 py-1 z-20 min-w-[140px]", children: [
2346
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2347
+ "button",
2348
+ {
2349
+ onClick: () => {
2350
+ onAddFolder();
2351
+ setShowAddMenu(false);
2352
+ },
2353
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-neutral-100 flex items-center gap-2",
2354
+ children: [
2355
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-amber-500", style: { fontSize: 16 }, children: "create_new_folder" }),
2356
+ "New Folder"
2357
+ ]
2358
+ }
2359
+ ),
2360
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "h-px bg-neutral-200 my-1" }),
2361
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2362
+ "button",
2363
+ {
2364
+ onClick: () => {
2365
+ onAddItem("image");
2366
+ setShowAddMenu(false);
2367
+ },
2368
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-neutral-100 flex items-center gap-2",
2369
+ children: [
2370
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-blue-500", style: { fontSize: 16 }, children: "image" }),
2371
+ "Image"
2372
+ ]
2373
+ }
2374
+ ),
2375
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2376
+ "button",
2377
+ {
2378
+ onClick: () => {
2379
+ onAddItem("text");
2380
+ setShowAddMenu(false);
2381
+ },
2382
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-neutral-100 flex items-center gap-2",
2383
+ children: [
2384
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-green-500", style: { fontSize: 16 }, children: "text_fields" }),
2385
+ "Text"
2386
+ ]
2387
+ }
2388
+ ),
2389
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2390
+ "button",
2391
+ {
2392
+ onClick: () => {
2393
+ onAddItem("shape");
2394
+ setShowAddMenu(false);
2395
+ },
2396
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-neutral-100 flex items-center gap-2",
2397
+ children: [
2398
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-purple-500", style: { fontSize: 16 }, children: "category" }),
2399
+ "Shape"
2400
+ ]
2401
+ }
2402
+ )
2403
+ ] })
2404
+ ] })
2405
+ ] })
2406
+ ] }),
2407
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 overflow-y-auto", children: layers.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col items-center justify-center h-full text-neutral-400 text-sm", children: [
2408
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons mb-2", style: { fontSize: 32 }, children: "layers" }),
2409
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "No layers yet" }),
2410
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs mt-1", children: "Click + to add a layer" })
2411
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "py-1", children: layers.map((layer) => renderLayer(layer, 0)) }) }),
2412
+ selectedIds.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "px-3 py-2 border-t border-neutral-200 bg-neutral-50", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-neutral-500", children: [
2413
+ selectedIds.length,
2414
+ " layer",
2415
+ selectedIds.length > 1 ? "s" : "",
2416
+ " selected"
2417
+ ] }) })
2418
+ ] });
2419
+ }
2420
+
2421
+ // src/components/animation/timeline/Timeline.tsx
2422
+ var import_react14 = __toESM(require("react"));
2423
+
2424
+ // src/components/animation/timeline/TimelineRuler.tsx
2425
+ var import_react10 = __toESM(require("react"));
2426
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2427
+ function TimelineRuler({
2428
+ duration,
2429
+ pixelsPerSecond,
2430
+ scrollX,
2431
+ viewportWidth,
2432
+ onSeek
2433
+ }) {
2434
+ const rulerRef = import_react10.default.useRef(null);
2435
+ const totalWidth = duration / 1e3 * pixelsPerSecond;
2436
+ const startTime = scrollX / pixelsPerSecond * 1e3;
2437
+ const endTime = (scrollX + viewportWidth) / pixelsPerSecond * 1e3;
2438
+ const getTickInterval = () => {
2439
+ if (pixelsPerSecond >= 200) {
2440
+ return { major: 1e3, minor: 100 };
2441
+ } else if (pixelsPerSecond >= 100) {
2442
+ return { major: 1e3, minor: 500 };
2443
+ } else if (pixelsPerSecond >= 50) {
2444
+ return { major: 5e3, minor: 1e3 };
2445
+ } else if (pixelsPerSecond >= 20) {
2446
+ return { major: 1e4, minor: 5e3 };
2447
+ } else {
2448
+ return { major: 3e4, minor: 1e4 };
2449
+ }
2450
+ };
2451
+ const { major, minor } = getTickInterval();
2452
+ const ticks = [];
2453
+ const firstTick = Math.floor(startTime / minor) * minor;
2454
+ for (let time = firstTick; time <= Math.min(endTime + minor, duration); time += minor) {
2455
+ ticks.push({
2456
+ time,
2457
+ isMajor: time % major === 0
2458
+ });
2459
+ }
2460
+ const handleClick = (e) => {
2461
+ if (!rulerRef.current) return;
2462
+ const rect = rulerRef.current.getBoundingClientRect();
2463
+ const x = e.clientX - rect.left + scrollX;
2464
+ const time = x / pixelsPerSecond * 1e3;
2465
+ onSeek(Math.max(0, Math.min(time, duration)));
2466
+ };
2467
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2468
+ "div",
2469
+ {
2470
+ ref: rulerRef,
2471
+ className: "relative h-6 bg-neutral-100 border-b border-neutral-300 cursor-pointer select-none",
2472
+ style: { width: totalWidth },
2473
+ onClick: handleClick,
2474
+ children: ticks.map(({ time, isMajor }) => {
2475
+ const x = time / 1e3 * pixelsPerSecond;
2476
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2477
+ "div",
2478
+ {
2479
+ className: "absolute top-0",
2480
+ style: { left: x },
2481
+ children: [
2482
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2483
+ "div",
2484
+ {
2485
+ className: `w-px ${isMajor ? "h-4 bg-neutral-400" : "h-2 bg-neutral-300"}`,
2486
+ style: { marginTop: isMajor ? 0 : 8 }
2487
+ }
2488
+ ),
2489
+ isMajor && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2490
+ "span",
2491
+ {
2492
+ className: "absolute text-[10px] text-neutral-500 whitespace-nowrap",
2493
+ style: {
2494
+ top: 12,
2495
+ left: 2
2496
+ },
2497
+ children: formatTimecode(time)
2498
+ }
2499
+ )
2500
+ ]
2501
+ },
2502
+ time
2503
+ );
2504
+ })
2505
+ }
2506
+ );
2507
+ }
2508
+
2509
+ // src/components/animation/timeline/Playhead.tsx
2510
+ var import_jsx_runtime8 = require("react/jsx-runtime");
2511
+ function Playhead({ currentTime, pixelsPerSecond, height, onSeek }) {
2512
+ const x = currentTime / 1e3 * pixelsPerSecond;
2513
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2514
+ "div",
2515
+ {
2516
+ className: "absolute top-0 pointer-events-none z-20",
2517
+ style: {
2518
+ left: x,
2519
+ height,
2520
+ transform: "translateX(-50%)"
2521
+ },
2522
+ children: [
2523
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "relative", children: [
2524
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2525
+ "div",
2526
+ {
2527
+ className: "absolute -top-1 left-1/2 -translate-x-1/2 bg-red-500 text-white text-[10px] px-1 rounded whitespace-nowrap",
2528
+ style: { transform: "translateX(-50%)" },
2529
+ children: formatTimecode(currentTime)
2530
+ }
2531
+ ),
2532
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2533
+ "div",
2534
+ {
2535
+ className: "absolute top-4 left-1/2 border-l-4 border-r-4 border-t-6 border-l-transparent border-r-transparent border-t-red-500",
2536
+ style: { transform: "translateX(-50%)" }
2537
+ }
2538
+ )
2539
+ ] }),
2540
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2541
+ "div",
2542
+ {
2543
+ className: "w-0.5 bg-red-500",
2544
+ style: { height: height - 20, marginTop: 20 }
2545
+ }
2546
+ )
2547
+ ]
2548
+ }
2549
+ );
2550
+ }
2551
+
2552
+ // src/components/animation/timeline/KeyframeTrack.tsx
2553
+ var import_react12 = __toESM(require("react"));
2554
+
2555
+ // src/components/animation/timeline/KeyframeDiamond.tsx
2556
+ var import_react11 = __toESM(require("react"));
2557
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2558
+ function KeyframeDiamond({
2559
+ keyframe,
2560
+ pixelsPerSecond,
2561
+ isSelected,
2562
+ onSelect,
2563
+ onMove,
2564
+ onDelete,
2565
+ snapToGrid
2566
+ }) {
2567
+ const [isDragging, setIsDragging] = import_react11.default.useState(false);
2568
+ const [dragStartX, setDragStartX] = import_react11.default.useState(0);
2569
+ const [originalTime, setOriginalTime] = import_react11.default.useState(keyframe.time);
2570
+ const x = keyframe.time / 1e3 * pixelsPerSecond;
2571
+ const handleMouseDown = (e) => {
2572
+ e.stopPropagation();
2573
+ onSelect(keyframe.id, e.shiftKey || e.ctrlKey || e.metaKey);
2574
+ setIsDragging(true);
2575
+ setDragStartX(e.clientX);
2576
+ setOriginalTime(keyframe.time);
2577
+ };
2578
+ import_react11.default.useEffect(() => {
2579
+ if (!isDragging) return;
2580
+ const handleMouseMove = (e) => {
2581
+ const deltaX = e.clientX - dragStartX;
2582
+ const deltaTime = deltaX / pixelsPerSecond * 1e3;
2583
+ let newTime = Math.max(0, originalTime + deltaTime);
2584
+ if (snapToGrid) {
2585
+ newTime = snapToGrid(newTime);
2586
+ }
2587
+ onMove(keyframe.id, newTime);
2588
+ };
2589
+ const handleMouseUp = () => {
2590
+ setIsDragging(false);
2591
+ };
2592
+ document.addEventListener("mousemove", handleMouseMove);
2593
+ document.addEventListener("mouseup", handleMouseUp);
2594
+ return () => {
2595
+ document.removeEventListener("mousemove", handleMouseMove);
2596
+ document.removeEventListener("mouseup", handleMouseUp);
2597
+ };
2598
+ }, [isDragging, dragStartX, originalTime, pixelsPerSecond, snapToGrid, onMove, keyframe.id]);
2599
+ const handleKeyDown = (e) => {
2600
+ if (e.key === "Delete" || e.key === "Backspace") {
2601
+ e.preventDefault();
2602
+ onDelete(keyframe.id);
2603
+ }
2604
+ };
2605
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2606
+ "div",
2607
+ {
2608
+ className: `
2609
+ absolute top-1/2 -translate-y-1/2 w-3 h-3 cursor-pointer
2610
+ transform rotate-45 transition-colors
2611
+ ${isSelected ? "bg-blue-500 ring-2 ring-blue-300" : "bg-amber-500 hover:bg-amber-400"}
2612
+ ${isDragging ? "scale-125" : ""}
2613
+ `,
2614
+ style: {
2615
+ left: x,
2616
+ transform: "translateX(-50%) translateY(-50%) rotate(45deg)"
2617
+ },
2618
+ onMouseDown: handleMouseDown,
2619
+ onKeyDown: handleKeyDown,
2620
+ onContextMenu: (e) => {
2621
+ e.preventDefault();
2622
+ onDelete(keyframe.id);
2623
+ },
2624
+ tabIndex: 0,
2625
+ title: `Keyframe at ${Math.round(keyframe.time)}ms`
2626
+ }
2627
+ );
2628
+ }
2629
+
2630
+ // src/components/animation/timeline/KeyframeTrack.tsx
2631
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2632
+ function KeyframeTrack({
2633
+ layer,
2634
+ depth,
2635
+ duration,
2636
+ pixelsPerSecond,
2637
+ selectedKeyframeIds,
2638
+ isLayerSelected,
2639
+ onKeyframeSelect,
2640
+ onKeyframeMove,
2641
+ onKeyframeAdd,
2642
+ onKeyframeDelete,
2643
+ snapToGrid
2644
+ }) {
2645
+ const trackRef = import_react12.default.useRef(null);
2646
+ const totalWidth = duration / 1e3 * pixelsPerSecond;
2647
+ const handleDoubleClick = (e) => {
2648
+ if (!trackRef.current) return;
2649
+ const rect = trackRef.current.getBoundingClientRect();
2650
+ const x = e.clientX - rect.left;
2651
+ let time = x / pixelsPerSecond * 1e3;
2652
+ if (snapToGrid) {
2653
+ time = snapToGrid(time);
2654
+ }
2655
+ onKeyframeAdd(layer.id, time);
2656
+ };
2657
+ const keyframes = layer.keyframes || [];
2658
+ const isFolder = layer.type === "folder";
2659
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2660
+ "div",
2661
+ {
2662
+ ref: trackRef,
2663
+ className: `
2664
+ relative h-7 border-b border-neutral-200
2665
+ ${isLayerSelected ? "bg-blue-500/10" : "hover:bg-neutral-50"}
2666
+ ${!layer.visible ? "opacity-50" : ""}
2667
+ `,
2668
+ style: {
2669
+ width: totalWidth,
2670
+ paddingLeft: depth * 16
2671
+ },
2672
+ onDoubleClick: handleDoubleClick,
2673
+ children: [
2674
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2675
+ "div",
2676
+ {
2677
+ className: `
2678
+ absolute top-1 bottom-1 left-0 right-0 rounded
2679
+ ${isFolder ? "bg-amber-100/50" : "bg-neutral-100"}
2680
+ `,
2681
+ style: { marginLeft: depth * 16 }
2682
+ }
2683
+ ),
2684
+ keyframes.map((kf) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2685
+ KeyframeDiamond,
2686
+ {
2687
+ keyframe: kf,
2688
+ pixelsPerSecond,
2689
+ isSelected: selectedKeyframeIds.includes(kf.id),
2690
+ onSelect: onKeyframeSelect,
2691
+ onMove: (kfId, newTime) => onKeyframeMove(layer.id, kfId, newTime),
2692
+ onDelete: (kfId) => onKeyframeDelete(layer.id, kfId),
2693
+ snapToGrid
2694
+ },
2695
+ kf.id
2696
+ )),
2697
+ keyframes.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2698
+ "svg",
2699
+ {
2700
+ className: "absolute top-0 left-0 pointer-events-none",
2701
+ style: { width: totalWidth, height: "100%" },
2702
+ children: keyframes.slice(0, -1).map((kf, i) => {
2703
+ const nextKf = keyframes[i + 1];
2704
+ const x1 = kf.time / 1e3 * pixelsPerSecond;
2705
+ const x2 = nextKf.time / 1e3 * pixelsPerSecond;
2706
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2707
+ "line",
2708
+ {
2709
+ x1,
2710
+ y1: "50%",
2711
+ x2,
2712
+ y2: "50%",
2713
+ stroke: isFolder ? "#f59e0b" : "#6b7280",
2714
+ strokeWidth: 1,
2715
+ strokeDasharray: "2,2"
2716
+ },
2717
+ `${kf.id}-${nextKf.id}`
2718
+ );
2719
+ })
2720
+ }
2721
+ )
2722
+ ]
2723
+ }
2724
+ );
2725
+ }
2726
+
2727
+ // src/components/animation/timeline/AudioTrack.tsx
2728
+ var import_react13 = __toESM(require("react"));
2729
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2730
+ function AudioTrack({
2731
+ track,
2732
+ duration,
2733
+ pixelsPerSecond,
2734
+ waveformData,
2735
+ isSelected,
2736
+ onSelect,
2737
+ onMuteToggle,
2738
+ onVolumeChange
2739
+ }) {
2740
+ const trackRef = import_react13.default.useRef(null);
2741
+ const canvasRef = import_react13.default.useRef(null);
2742
+ const totalWidth = duration / 1e3 * pixelsPerSecond;
2743
+ const trackOffset = track.offset ?? 0;
2744
+ const trackStart = trackOffset / 1e3 * pixelsPerSecond;
2745
+ import_react13.default.useEffect(() => {
2746
+ const canvas = canvasRef.current;
2747
+ if (!canvas || !waveformData || waveformData.length === 0) return;
2748
+ const ctx = canvas.getContext("2d");
2749
+ if (!ctx) return;
2750
+ const { width, height } = canvas;
2751
+ ctx.clearRect(0, 0, width, height);
2752
+ ctx.fillStyle = track.muted ? "#d1d5db" : "#3b82f6";
2753
+ const barWidth = width / waveformData.length;
2754
+ const centerY = height / 2;
2755
+ for (let i = 0; i < waveformData.length; i++) {
2756
+ const barHeight = waveformData[i] * height * 0.8;
2757
+ ctx.fillRect(
2758
+ i * barWidth,
2759
+ centerY - barHeight / 2,
2760
+ Math.max(1, barWidth - 1),
2761
+ barHeight
2762
+ );
2763
+ }
2764
+ }, [waveformData, track.muted]);
2765
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2766
+ "div",
2767
+ {
2768
+ ref: trackRef,
2769
+ className: `
2770
+ relative h-12 border-b border-neutral-200
2771
+ ${isSelected ? "bg-blue-500/10" : "hover:bg-neutral-50"}
2772
+ `,
2773
+ style: { width: totalWidth },
2774
+ onClick: onSelect,
2775
+ children: [
2776
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "absolute left-0 top-0 bottom-0 w-8 bg-neutral-100 border-r border-neutral-200 flex flex-col items-center justify-center gap-0.5 z-10", children: [
2777
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "material-icons text-blue-500", style: { fontSize: 14 }, children: "audiotrack" }),
2778
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2779
+ "button",
2780
+ {
2781
+ onClick: (e) => {
2782
+ e.stopPropagation();
2783
+ onMuteToggle();
2784
+ },
2785
+ className: "p-0.5 hover:bg-neutral-200 rounded",
2786
+ title: track.muted ? "Unmute" : "Mute",
2787
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "material-icons text-neutral-400", style: { fontSize: 12 }, children: track.muted ? "volume_off" : "volume_up" })
2788
+ }
2789
+ )
2790
+ ] }),
2791
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2792
+ "div",
2793
+ {
2794
+ className: `
2795
+ absolute top-1 bottom-1 rounded overflow-hidden
2796
+ ${track.muted ? "bg-neutral-200" : "bg-blue-100"}
2797
+ `,
2798
+ style: {
2799
+ left: trackStart + 32,
2800
+ width: Math.max(50, totalWidth - trackStart - 32)
2801
+ },
2802
+ children: [
2803
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "absolute top-0 left-1 text-[10px] text-neutral-500 truncate max-w-[100px]", children: track.name }),
2804
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2805
+ "canvas",
2806
+ {
2807
+ ref: canvasRef,
2808
+ className: "absolute inset-0",
2809
+ width: Math.max(100, totalWidth - trackStart - 32),
2810
+ height: 40
2811
+ }
2812
+ ),
2813
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "absolute bottom-0 right-1 text-[10px] text-neutral-400", children: [
2814
+ Math.round((track.volume ?? 1) * 100),
2815
+ "%"
2816
+ ] })
2817
+ ]
2818
+ }
2819
+ )
2820
+ ]
2821
+ }
2822
+ );
2823
+ }
2824
+
2825
+ // src/components/animation/timeline/Timeline.tsx
2826
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2827
+ function Timeline({
2828
+ composition,
2829
+ currentTime,
2830
+ isPlaying,
2831
+ zoom,
2832
+ scrollX,
2833
+ selectedLayerIds,
2834
+ selectedKeyframeIds,
2835
+ snapToGrid,
2836
+ gridSize,
2837
+ onSeek,
2838
+ onZoomChange,
2839
+ onScrollChange,
2840
+ onKeyframeSelect,
2841
+ onKeyframeMove,
2842
+ onKeyframeAdd,
2843
+ onKeyframeDelete,
2844
+ onPlay,
2845
+ onPause,
2846
+ onStop
2847
+ }) {
2848
+ const containerRef = import_react14.default.useRef(null);
2849
+ const [viewportWidth, setViewportWidth] = import_react14.default.useState(800);
2850
+ const pixelsPerSecond = zoom;
2851
+ const totalWidth = composition.duration / 1e3 * pixelsPerSecond;
2852
+ const flatLayers = import_react14.default.useMemo(() => {
2853
+ return flattenLayers(composition.layers);
2854
+ }, [composition.layers]);
2855
+ const snapToGridFn = import_react14.default.useCallback(
2856
+ (time) => {
2857
+ if (!snapToGrid) return time;
2858
+ return Math.round(time / gridSize) * gridSize;
2859
+ },
2860
+ [snapToGrid, gridSize]
2861
+ );
2862
+ import_react14.default.useEffect(() => {
2863
+ const updateWidth = () => {
2864
+ if (containerRef.current) {
2865
+ setViewportWidth(containerRef.current.clientWidth - 150);
2866
+ }
2867
+ };
2868
+ updateWidth();
2869
+ window.addEventListener("resize", updateWidth);
2870
+ return () => window.removeEventListener("resize", updateWidth);
2871
+ }, []);
2872
+ const handleScroll = (e) => {
2873
+ onScrollChange(e.currentTarget.scrollLeft);
2874
+ };
2875
+ const handleWheel = (e) => {
2876
+ if (e.ctrlKey || e.metaKey) {
2877
+ e.preventDefault();
2878
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
2879
+ const newZoom = Math.max(10, Math.min(500, zoom * delta));
2880
+ onZoomChange(newZoom);
2881
+ }
2882
+ };
2883
+ const layerHeight = 28;
2884
+ const audioHeight = 48;
2885
+ const totalHeight = 24 + // Ruler
2886
+ flatLayers.length * layerHeight + (composition.audioTracks?.length ?? 0) * audioHeight;
2887
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col h-full bg-white border-t border-neutral-200", children: [
2888
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-neutral-200 bg-neutral-50", children: [
2889
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-1", children: [
2890
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2891
+ "button",
2892
+ {
2893
+ onClick: onStop,
2894
+ className: "p-1.5 hover:bg-neutral-200 rounded",
2895
+ title: "Stop (go to start)",
2896
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons", style: { fontSize: 18 }, children: "stop" })
2897
+ }
2898
+ ),
2899
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2900
+ "button",
2901
+ {
2902
+ onClick: isPlaying ? onPause : onPlay,
2903
+ className: "p-1.5 hover:bg-neutral-200 rounded",
2904
+ title: isPlaying ? "Pause" : "Play",
2905
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons", style: { fontSize: 18 }, children: isPlaying ? "pause" : "play_arrow" })
2906
+ }
2907
+ )
2908
+ ] }),
2909
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "px-2 py-1 bg-neutral-900 text-white font-mono text-sm rounded", children: [
2910
+ formatTimecode(currentTime),
2911
+ " / ",
2912
+ formatTimecode(composition.duration)
2913
+ ] }),
2914
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex-1" }),
2915
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2", children: [
2916
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2917
+ "button",
2918
+ {
2919
+ onClick: () => onZoomChange(Math.max(10, zoom * 0.8)),
2920
+ className: "p-1 hover:bg-neutral-200 rounded",
2921
+ title: "Zoom out",
2922
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons", style: { fontSize: 16 }, children: "remove" })
2923
+ }
2924
+ ),
2925
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-xs text-neutral-500 w-16 text-center", children: [
2926
+ Math.round(zoom),
2927
+ "px/s"
2928
+ ] }),
2929
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2930
+ "button",
2931
+ {
2932
+ onClick: () => onZoomChange(Math.min(500, zoom * 1.25)),
2933
+ className: "p-1 hover:bg-neutral-200 rounded",
2934
+ title: "Zoom in",
2935
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons", style: { fontSize: 16 }, children: "add" })
2936
+ }
2937
+ )
2938
+ ] }),
2939
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2940
+ "button",
2941
+ {
2942
+ onClick: () => {
2943
+ },
2944
+ className: `p-1.5 rounded ${snapToGrid ? "bg-blue-100 text-blue-600" : "hover:bg-neutral-200"}`,
2945
+ title: "Snap to grid",
2946
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons", style: { fontSize: 16 }, children: "grid_on" })
2947
+ }
2948
+ )
2949
+ ] }),
2950
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-1 overflow-hidden", children: [
2951
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "w-[150px] flex-shrink-0 border-r border-neutral-200 overflow-y-auto", children: [
2952
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "h-6 bg-neutral-100 border-b border-neutral-300 px-2 flex items-center", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-neutral-500", children: "Layers" }) }),
2953
+ flatLayers.map((layer) => {
2954
+ const depth = getLayerDepth(layer, composition.layers);
2955
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2956
+ "div",
2957
+ {
2958
+ className: `
2959
+ h-7 border-b border-neutral-200 px-2 flex items-center
2960
+ ${selectedLayerIds.includes(layer.id) ? "bg-blue-500/10" : ""}
2961
+ ${!layer.visible ? "opacity-50" : ""}
2962
+ `,
2963
+ style: { paddingLeft: depth * 12 + 8 },
2964
+ children: [
2965
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons text-neutral-400 mr-1", style: { fontSize: 12 }, children: layer.type === "folder" ? "folder" : "lens" }),
2966
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-neutral-700 truncate", children: layer.name })
2967
+ ]
2968
+ },
2969
+ layer.id
2970
+ );
2971
+ }),
2972
+ composition.audioTracks?.map((track) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2973
+ "div",
2974
+ {
2975
+ className: "h-12 border-b border-neutral-200 px-2 flex items-center",
2976
+ children: [
2977
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons text-blue-500 mr-1", style: { fontSize: 12 }, children: "audiotrack" }),
2978
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-neutral-700 truncate", children: track.name })
2979
+ ]
2980
+ },
2981
+ track.id
2982
+ ))
2983
+ ] }),
2984
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2985
+ "div",
2986
+ {
2987
+ ref: containerRef,
2988
+ className: "flex-1 overflow-auto relative",
2989
+ onScroll: handleScroll,
2990
+ onWheel: handleWheel,
2991
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { width: totalWidth, minHeight: totalHeight }, children: [
2992
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2993
+ TimelineRuler,
2994
+ {
2995
+ duration: composition.duration,
2996
+ pixelsPerSecond,
2997
+ scrollX,
2998
+ viewportWidth,
2999
+ onSeek
3000
+ }
3001
+ ),
3002
+ flatLayers.map((layer) => {
3003
+ const depth = getLayerDepth(layer, composition.layers);
3004
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3005
+ KeyframeTrack,
3006
+ {
3007
+ layer,
3008
+ depth,
3009
+ duration: composition.duration,
3010
+ pixelsPerSecond,
3011
+ selectedKeyframeIds,
3012
+ isLayerSelected: selectedLayerIds.includes(layer.id),
3013
+ onKeyframeSelect,
3014
+ onKeyframeMove,
3015
+ onKeyframeAdd,
3016
+ onKeyframeDelete,
3017
+ snapToGrid: snapToGridFn
3018
+ },
3019
+ layer.id
3020
+ );
3021
+ }),
3022
+ composition.audioTracks?.map((track) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3023
+ AudioTrack,
3024
+ {
3025
+ track,
3026
+ duration: composition.duration,
3027
+ pixelsPerSecond,
3028
+ isSelected: false,
3029
+ onSelect: () => {
3030
+ },
3031
+ onMuteToggle: () => {
3032
+ },
3033
+ onVolumeChange: () => {
3034
+ }
3035
+ },
3036
+ track.id
3037
+ )),
3038
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3039
+ Playhead,
3040
+ {
3041
+ currentTime,
3042
+ pixelsPerSecond,
3043
+ height: totalHeight
3044
+ }
3045
+ )
3046
+ ] })
3047
+ }
3048
+ )
3049
+ ] })
3050
+ ] });
3051
+ }
3052
+ function getLayerDepth(target, layers, currentDepth = 0) {
3053
+ for (const layer of layers) {
3054
+ if (layer.id === target.id) return currentDepth;
3055
+ if (layer.type === "folder") {
3056
+ const depth = getLayerDepth(target, layer.children, currentDepth + 1);
3057
+ if (depth >= 0) return depth;
3058
+ }
3059
+ }
3060
+ return 0;
3061
+ }
3062
+
3063
+ // src/components/animation/canvas/PreviewCanvas.tsx
3064
+ var import_react16 = __toESM(require("react"));
3065
+
3066
+ // src/components/animation/hooks/useKeyframes.ts
3067
+ var import_react15 = require("react");
3068
+ var easingFunctions = {
3069
+ linear: (t) => t,
3070
+ ease: (t) => t * t * (3 - 2 * t),
3071
+ "ease-in": (t) => t * t,
3072
+ "ease-out": (t) => t * (2 - t),
3073
+ "ease-in-out": (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
3074
+ "step-start": () => 1,
3075
+ "step-end": (t) => t >= 1 ? 1 : 0
3076
+ };
3077
+ function lerp(a, b, t) {
3078
+ return a + (b - a) * t;
3079
+ }
3080
+ function getEasingFn(easing) {
3081
+ return easingFunctions[easing || "linear"];
3082
+ }
3083
+ function interpolateKeyframes(keyframes, time) {
3084
+ if (!keyframes || keyframes.length === 0) {
3085
+ return { visible: true, opacity: 1 };
3086
+ }
3087
+ const sorted = [...keyframes].sort((a, b) => a.time - b.time);
3088
+ if (time <= sorted[0].time) {
3089
+ return { ...sorted[0].properties };
3090
+ }
3091
+ if (time >= sorted[sorted.length - 1].time) {
3092
+ return { ...sorted[sorted.length - 1].properties };
3093
+ }
3094
+ let prevKf = sorted[0];
3095
+ let nextKf = sorted[1];
3096
+ for (let i = 1; i < sorted.length; i++) {
3097
+ if (sorted[i].time >= time) {
3098
+ prevKf = sorted[i - 1];
3099
+ nextKf = sorted[i];
3100
+ break;
3101
+ }
3102
+ }
3103
+ const duration = nextKf.time - prevKf.time;
3104
+ const elapsed = time - prevKf.time;
3105
+ const rawT = duration > 0 ? elapsed / duration : 0;
3106
+ const easingFn = getEasingFn(prevKf.easing);
3107
+ const t = easingFn(rawT);
3108
+ const result = {};
3109
+ if (prevKf.properties.visible !== void 0 || nextKf.properties.visible !== void 0) {
3110
+ result.visible = rawT < 1 ? prevKf.properties.visible : nextKf.properties.visible;
3111
+ }
3112
+ const numericProps = ["opacity", "x", "y", "scaleX", "scaleY", "rotation", "volume"];
3113
+ for (const prop of numericProps) {
3114
+ const prevVal = prevKf.properties[prop];
3115
+ const nextVal = nextKf.properties[prop];
3116
+ if (prevVal !== void 0 && nextVal !== void 0) {
3117
+ result[prop] = lerp(prevVal, nextVal, t);
3118
+ } else if (prevVal !== void 0) {
3119
+ result[prop] = prevVal;
3120
+ } else if (nextVal !== void 0) {
3121
+ result[prop] = nextVal;
3122
+ }
3123
+ }
3124
+ return result;
3125
+ }
3126
+ function getLayerPropertiesAtTime(layer, time) {
3127
+ const keyframeProps = interpolateKeyframes(layer.keyframes || [], time);
3128
+ return {
3129
+ layerVisible: layer.visible,
3130
+ layerOpacity: layer.opacity,
3131
+ visible: keyframeProps.visible ?? true,
3132
+ opacity: (keyframeProps.opacity ?? 1) * layer.opacity,
3133
+ x: keyframeProps.x,
3134
+ y: keyframeProps.y,
3135
+ scaleX: keyframeProps.scaleX,
3136
+ scaleY: keyframeProps.scaleY,
3137
+ rotation: keyframeProps.rotation,
3138
+ volume: keyframeProps.volume
3139
+ };
3140
+ }
3141
+ function useKeyframes({ layers, currentTime }) {
3142
+ const layerStates = (0, import_react15.useMemo)(() => {
3143
+ const states = [];
3144
+ const processLayer = (layer) => {
3145
+ states.push({
3146
+ id: layer.id,
3147
+ properties: getLayerPropertiesAtTime(layer, currentTime)
3148
+ });
3149
+ if (layer.type === "folder") {
3150
+ for (const child of layer.children) {
3151
+ processLayer(child);
3152
+ }
3153
+ }
3154
+ };
3155
+ for (const layer of layers) {
3156
+ processLayer(layer);
3157
+ }
3158
+ return states;
3159
+ }, [layers, currentTime]);
3160
+ const layerStatesMap = (0, import_react15.useMemo)(() => {
3161
+ const map = /* @__PURE__ */ new Map();
3162
+ for (const state of layerStates) {
3163
+ map.set(state.id, state);
3164
+ }
3165
+ return map;
3166
+ }, [layerStates]);
3167
+ const getLayerState = (layerId) => {
3168
+ return layerStatesMap.get(layerId);
3169
+ };
3170
+ return {
3171
+ layerStates,
3172
+ layerStatesMap,
3173
+ getLayerState
3174
+ };
3175
+ }
3176
+
3177
+ // src/components/animation/canvas/CanvasObject.tsx
3178
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3179
+ function CanvasObject({
3180
+ layer,
3181
+ computedProps,
3182
+ isSelected,
3183
+ canvasWidth,
3184
+ canvasHeight,
3185
+ onSelect
3186
+ }) {
3187
+ const { object } = layer;
3188
+ if (!layer.visible || computedProps.visible === false) {
3189
+ return null;
3190
+ }
3191
+ const x = (computedProps.x ?? object.position.x) / 100 * canvasWidth;
3192
+ const y = (computedProps.y ?? object.position.y) / 100 * canvasHeight;
3193
+ let width = "auto";
3194
+ let height = "auto";
3195
+ if (object.size) {
3196
+ if (object.size.unit === "percent") {
3197
+ width = object.size.width / 100 * canvasWidth;
3198
+ height = object.size.height / 100 * canvasHeight;
3199
+ } else {
3200
+ width = object.size.width;
3201
+ height = object.size.height;
3202
+ }
3203
+ }
3204
+ const scaleX = computedProps.scaleX ?? 1;
3205
+ const scaleY = computedProps.scaleY ?? 1;
3206
+ const rotation = computedProps.rotation ?? 0;
3207
+ const transform = `scale(${scaleX}, ${scaleY}) rotate(${rotation}deg)`;
3208
+ const opacity = (computedProps.opacity ?? 1) * layer.opacity;
3209
+ const style = {
3210
+ position: "absolute",
3211
+ left: x,
3212
+ top: y,
3213
+ width,
3214
+ height,
3215
+ transform,
3216
+ opacity,
3217
+ transformOrigin: "center center",
3218
+ cursor: "pointer",
3219
+ outline: isSelected ? "2px solid #3b82f6" : "none",
3220
+ outlineOffset: 2
3221
+ };
3222
+ const renderContent = () => {
3223
+ switch (object.type) {
3224
+ case "image":
3225
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3226
+ "img",
3227
+ {
3228
+ src: object.src,
3229
+ alt: object.alt || "",
3230
+ style: {
3231
+ width: "100%",
3232
+ height: "100%",
3233
+ objectFit: object.objectFit || "contain",
3234
+ objectPosition: object.objectPosition,
3235
+ borderRadius: object.borderRadius,
3236
+ filter: object.filter
3237
+ },
3238
+ draggable: false
3239
+ }
3240
+ );
3241
+ case "text":
3242
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3243
+ "div",
3244
+ {
3245
+ style: {
3246
+ fontSize: object.fontSize,
3247
+ fontFamily: object.fontFamily,
3248
+ fontWeight: object.fontWeight,
3249
+ fontStyle: object.fontStyle,
3250
+ textAlign: object.textAlign,
3251
+ color: object.color,
3252
+ backgroundColor: object.backgroundColor,
3253
+ lineHeight: object.lineHeight,
3254
+ letterSpacing: object.letterSpacing,
3255
+ textShadow: object.textShadow,
3256
+ padding: object.padding,
3257
+ whiteSpace: "pre-wrap"
3258
+ },
3259
+ children: object.content
3260
+ }
3261
+ );
3262
+ case "shape":
3263
+ return renderShape(object);
3264
+ case "video":
3265
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3266
+ "video",
3267
+ {
3268
+ src: object.src,
3269
+ poster: object.poster,
3270
+ muted: object.muted,
3271
+ loop: object.loop,
3272
+ style: {
3273
+ width: "100%",
3274
+ height: "100%",
3275
+ objectFit: object.objectFit || "contain"
3276
+ }
3277
+ }
3278
+ );
3279
+ case "audio":
3280
+ if (!object.showControls) return null;
3281
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3282
+ "audio",
3283
+ {
3284
+ src: object.src,
3285
+ controls: true,
3286
+ style: { width: "100%" }
3287
+ }
3288
+ );
3289
+ default:
3290
+ return null;
3291
+ }
3292
+ };
3293
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3294
+ "div",
3295
+ {
3296
+ style,
3297
+ onClick: (e) => {
3298
+ e.stopPropagation();
3299
+ onSelect(layer.id);
3300
+ },
3301
+ children: renderContent()
3302
+ }
3303
+ );
3304
+ }
3305
+ function renderShape(object) {
3306
+ const { shape, fill, stroke, strokeWidth, borderRadius } = object;
3307
+ const svgStyle = {
3308
+ width: "100%",
3309
+ height: "100%"
3310
+ };
3311
+ switch (shape) {
3312
+ case "rectangle":
3313
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3314
+ "div",
3315
+ {
3316
+ style: {
3317
+ width: "100%",
3318
+ height: "100%",
3319
+ backgroundColor: fill,
3320
+ border: stroke ? `${strokeWidth || 1}px solid ${stroke}` : void 0,
3321
+ borderRadius
3322
+ }
3323
+ }
3324
+ );
3325
+ case "circle":
3326
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("svg", { style: svgStyle, viewBox: "0 0 100 100", preserveAspectRatio: "none", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3327
+ "circle",
3328
+ {
3329
+ cx: "50",
3330
+ cy: "50",
3331
+ r: "48",
3332
+ fill,
3333
+ stroke,
3334
+ strokeWidth
3335
+ }
3336
+ ) });
3337
+ case "ellipse":
3338
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("svg", { style: svgStyle, viewBox: "0 0 100 100", preserveAspectRatio: "none", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3339
+ "ellipse",
3340
+ {
3341
+ cx: "50",
3342
+ cy: "50",
3343
+ rx: "48",
3344
+ ry: "48",
3345
+ fill,
3346
+ stroke,
3347
+ strokeWidth
3348
+ }
3349
+ ) });
3350
+ case "triangle":
3351
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("svg", { style: svgStyle, viewBox: "0 0 100 100", preserveAspectRatio: "none", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3352
+ "polygon",
3353
+ {
3354
+ points: "50,5 95,95 5,95",
3355
+ fill,
3356
+ stroke,
3357
+ strokeWidth
3358
+ }
3359
+ ) });
3360
+ default:
3361
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3362
+ "div",
3363
+ {
3364
+ style: {
3365
+ width: "100%",
3366
+ height: "100%",
3367
+ backgroundColor: fill || "#ccc"
3368
+ }
3369
+ }
3370
+ );
3371
+ }
3372
+ }
3373
+
3374
+ // src/components/animation/canvas/PreviewCanvas.tsx
3375
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3376
+ function PreviewCanvas({
3377
+ composition,
3378
+ currentTime,
3379
+ selectedLayerIds,
3380
+ onLayerSelect,
3381
+ onClearSelection
3382
+ }) {
3383
+ const containerRef = import_react16.default.useRef(null);
3384
+ const [containerSize, setContainerSize] = import_react16.default.useState({ width: 800, height: 450 });
3385
+ const { layerStatesMap } = useKeyframes({
3386
+ layers: composition.layers,
3387
+ currentTime
3388
+ });
3389
+ import_react16.default.useEffect(() => {
3390
+ const updateSize = () => {
3391
+ if (!containerRef.current) return;
3392
+ const containerWidth = containerRef.current.clientWidth;
3393
+ const containerHeight = containerRef.current.clientHeight;
3394
+ const padding = 40;
3395
+ const availableWidth = containerWidth - padding;
3396
+ const availableHeight = containerHeight - padding;
3397
+ const aspectRatio = composition.width / composition.height;
3398
+ const containerRatio = availableWidth / availableHeight;
3399
+ let canvasWidth;
3400
+ let canvasHeight;
3401
+ if (containerRatio > aspectRatio) {
3402
+ canvasHeight = availableHeight;
3403
+ canvasWidth = canvasHeight * aspectRatio;
3404
+ } else {
3405
+ canvasWidth = availableWidth;
3406
+ canvasHeight = canvasWidth / aspectRatio;
3407
+ }
3408
+ setContainerSize({ width: canvasWidth, height: canvasHeight });
3409
+ };
3410
+ updateSize();
3411
+ window.addEventListener("resize", updateSize);
3412
+ return () => window.removeEventListener("resize", updateSize);
3413
+ }, [composition.width, composition.height]);
3414
+ const renderableLayers = import_react16.default.useMemo(() => {
3415
+ const result = [];
3416
+ const collectLayers = (layers, parentVisible = true) => {
3417
+ for (const layer of layers) {
3418
+ const isVisible = parentVisible && layer.visible;
3419
+ if (layer.type === "folder") {
3420
+ collectLayers(layer.children, isVisible);
3421
+ } else if (isVisible) {
3422
+ result.push(layer);
3423
+ }
3424
+ }
3425
+ };
3426
+ collectLayers(composition.layers);
3427
+ return result;
3428
+ }, [composition.layers]);
3429
+ const renderBackground = () => {
3430
+ const bg = composition.background;
3431
+ if (!bg) return null;
3432
+ const style = {
3433
+ position: "absolute",
3434
+ inset: 0,
3435
+ opacity: bg.opacity ?? 1
3436
+ };
3437
+ switch (bg.type) {
3438
+ case "color":
3439
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { ...style, backgroundColor: bg.value } });
3440
+ case "gradient":
3441
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { ...style, background: bg.value } });
3442
+ case "image":
3443
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3444
+ "div",
3445
+ {
3446
+ style: {
3447
+ ...style,
3448
+ backgroundImage: `url(${bg.value})`,
3449
+ backgroundSize: "cover",
3450
+ backgroundPosition: "center",
3451
+ filter: bg.blur ? `blur(${bg.blur}px)` : void 0
3452
+ }
3453
+ }
3454
+ );
3455
+ case "video":
3456
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3457
+ "video",
3458
+ {
3459
+ src: bg.value,
3460
+ autoPlay: true,
3461
+ muted: true,
3462
+ loop: true,
3463
+ style: {
3464
+ ...style,
3465
+ objectFit: "cover",
3466
+ filter: bg.blur ? `blur(${bg.blur}px)` : void 0
3467
+ }
3468
+ }
3469
+ );
3470
+ default:
3471
+ return null;
3472
+ }
3473
+ };
3474
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3475
+ "div",
3476
+ {
3477
+ ref: containerRef,
3478
+ className: "flex-1 flex items-center justify-center bg-neutral-800 overflow-hidden",
3479
+ onClick: onClearSelection,
3480
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
3481
+ "div",
3482
+ {
3483
+ className: "relative bg-white shadow-2xl overflow-hidden",
3484
+ style: {
3485
+ width: containerSize.width,
3486
+ height: containerSize.height
3487
+ },
3488
+ children: [
3489
+ renderBackground(),
3490
+ composition.background?.overlay && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3491
+ "div",
3492
+ {
3493
+ className: "absolute inset-0",
3494
+ style: { backgroundColor: composition.background.overlay }
3495
+ }
3496
+ ),
3497
+ renderableLayers.map((layer) => {
3498
+ const state = layerStatesMap.get(layer.id);
3499
+ if (!state) return null;
3500
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3501
+ CanvasObject,
3502
+ {
3503
+ layer,
3504
+ computedProps: state.properties,
3505
+ isSelected: selectedLayerIds.includes(layer.id),
3506
+ canvasWidth: containerSize.width,
3507
+ canvasHeight: containerSize.height,
3508
+ onSelect: onLayerSelect
3509
+ },
3510
+ layer.id
3511
+ );
3512
+ }),
3513
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "absolute inset-0 pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute bottom-2 right-2 text-xs text-neutral-400 bg-black/50 px-1 rounded", children: [
3514
+ composition.width,
3515
+ " \xD7 ",
3516
+ composition.height
3517
+ ] }) })
3518
+ ]
3519
+ }
3520
+ )
3521
+ }
3522
+ );
3523
+ }
3524
+
3525
+ // src/components/animation/properties/PropertiesPanel.tsx
3526
+ var import_react17 = __toESM(require("react"));
3527
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3528
+ function PropertiesPanel({
3529
+ selectedLayers,
3530
+ currentTime,
3531
+ onUpdateLayer,
3532
+ onAddKeyframe
3533
+ }) {
3534
+ if (selectedLayers.length === 0) {
3535
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "w-[250px] flex-shrink-0 bg-white border-l border-neutral-200 flex flex-col", children: [
3536
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "px-3 py-2 border-b border-neutral-200", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-semibold text-neutral-700", children: "Properties" }) }),
3537
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex-1 flex items-center justify-center text-neutral-400 text-sm p-4 text-center", children: "Select a layer to edit its properties" })
3538
+ ] });
3539
+ }
3540
+ if (selectedLayers.length > 1) {
3541
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "w-[250px] flex-shrink-0 bg-white border-l border-neutral-200 flex flex-col", children: [
3542
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "px-3 py-2 border-b border-neutral-200", children: [
3543
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-semibold text-neutral-700", children: "Properties" }),
3544
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-xs text-neutral-400", children: [
3545
+ selectedLayers.length,
3546
+ " layers selected"
3547
+ ] })
3548
+ ] }),
3549
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex-1 overflow-y-auto p-3", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-neutral-500", children: "Multi-selection editing coming soon." }) })
3550
+ ] });
3551
+ }
3552
+ const layer = selectedLayers[0];
3553
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "w-[250px] flex-shrink-0 bg-white border-l border-neutral-200 flex flex-col", children: [
3554
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "px-3 py-2 border-b border-neutral-200", children: [
3555
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-semibold text-neutral-700", children: "Properties" }),
3556
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-xs text-neutral-400 truncate", children: layer.name })
3557
+ ] }),
3558
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 overflow-y-auto", children: [
3559
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(PropertySection, { title: "Layer", children: [
3560
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Name", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3561
+ "input",
3562
+ {
3563
+ type: "text",
3564
+ value: layer.name,
3565
+ onChange: (e) => onUpdateLayer(layer.id, { name: e.target.value }),
3566
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded focus:outline-none focus:border-blue-500"
3567
+ }
3568
+ ) }),
3569
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Visible", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3570
+ "input",
3571
+ {
3572
+ type: "checkbox",
3573
+ checked: layer.visible,
3574
+ onChange: (e) => onUpdateLayer(layer.id, { visible: e.target.checked }),
3575
+ className: "w-4 h-4"
3576
+ }
3577
+ ) }),
3578
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Locked", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3579
+ "input",
3580
+ {
3581
+ type: "checkbox",
3582
+ checked: layer.locked,
3583
+ onChange: (e) => onUpdateLayer(layer.id, { locked: e.target.checked }),
3584
+ className: "w-4 h-4"
3585
+ }
3586
+ ) }),
3587
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Opacity", keyframeable: true, onAddKeyframe: () => onAddKeyframe(layer.id, "opacity"), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2", children: [
3588
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3589
+ "input",
3590
+ {
3591
+ type: "range",
3592
+ min: 0,
3593
+ max: 1,
3594
+ step: 0.01,
3595
+ value: layer.opacity,
3596
+ onChange: (e) => onUpdateLayer(layer.id, { opacity: parseFloat(e.target.value) }),
3597
+ className: "flex-1"
3598
+ }
3599
+ ),
3600
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-xs text-neutral-500 w-10 text-right", children: [
3601
+ Math.round(layer.opacity * 100),
3602
+ "%"
3603
+ ] })
3604
+ ] }) }),
3605
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Blend", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3606
+ "select",
3607
+ {
3608
+ value: layer.blendMode || "normal",
3609
+ onChange: (e) => onUpdateLayer(layer.id, { blendMode: e.target.value }),
3610
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded focus:outline-none focus:border-blue-500",
3611
+ children: [
3612
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "normal", children: "Normal" }),
3613
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "multiply", children: "Multiply" }),
3614
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "screen", children: "Screen" }),
3615
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "overlay", children: "Overlay" }),
3616
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "darken", children: "Darken" }),
3617
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "lighten", children: "Lighten" })
3618
+ ]
3619
+ }
3620
+ ) })
3621
+ ] }),
3622
+ layer.type === "item" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3623
+ ObjectProperties,
3624
+ {
3625
+ layer,
3626
+ onUpdate: (updates) => onUpdateLayer(layer.id, updates),
3627
+ onAddKeyframe: (prop) => onAddKeyframe(layer.id, prop)
3628
+ }
3629
+ ),
3630
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(PropertySection, { title: "Keyframes", children: [
3631
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "text-xs text-neutral-500", children: [
3632
+ layer.keyframes?.length ?? 0,
3633
+ " keyframe(s)"
3634
+ ] }),
3635
+ layer.keyframes && layer.keyframes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "mt-2 space-y-1", children: layer.keyframes.map((kf) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3636
+ "div",
3637
+ {
3638
+ className: "text-xs px-2 py-1 bg-neutral-50 rounded flex justify-between",
3639
+ children: [
3640
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: formatTimecode(kf.time) }),
3641
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-neutral-400", children: Object.keys(kf.properties).join(", ") })
3642
+ ]
3643
+ },
3644
+ kf.id
3645
+ )) })
3646
+ ] })
3647
+ ] })
3648
+ ] });
3649
+ }
3650
+ function PropertySection({ title, children }) {
3651
+ const [isOpen, setIsOpen] = import_react17.default.useState(true);
3652
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "border-b border-neutral-200", children: [
3653
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3654
+ "button",
3655
+ {
3656
+ onClick: () => setIsOpen(!isOpen),
3657
+ className: "w-full px-3 py-2 flex items-center justify-between hover:bg-neutral-50",
3658
+ children: [
3659
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-xs font-semibold text-neutral-600 uppercase", children: title }),
3660
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-icons text-neutral-400", style: { fontSize: 16 }, children: isOpen ? "expand_less" : "expand_more" })
3661
+ ]
3662
+ }
3663
+ ),
3664
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "px-3 pb-3 space-y-2", children })
3665
+ ] });
3666
+ }
3667
+ function PropertyRow({
3668
+ label,
3669
+ children,
3670
+ keyframeable,
3671
+ onAddKeyframe
3672
+ }) {
3673
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2", children: [
3674
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "w-16 flex items-center gap-1", children: [
3675
+ keyframeable && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3676
+ "button",
3677
+ {
3678
+ onClick: onAddKeyframe,
3679
+ className: "p-0.5 hover:bg-amber-100 rounded",
3680
+ title: "Add keyframe",
3681
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-icons text-amber-500", style: { fontSize: 12 }, children: "diamond" })
3682
+ }
3683
+ ),
3684
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-xs text-neutral-500", children: label })
3685
+ ] }),
3686
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex-1", children })
3687
+ ] });
3688
+ }
3689
+ function ObjectProperties({
3690
+ layer,
3691
+ onUpdate,
3692
+ onAddKeyframe
3693
+ }) {
3694
+ const { object } = layer;
3695
+ switch (object.type) {
3696
+ case "image":
3697
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(PropertySection, { title: "Image", children: [
3698
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Source", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3699
+ "input",
3700
+ {
3701
+ type: "text",
3702
+ value: object.src,
3703
+ onChange: (e) => onUpdate({ object: { ...object, src: e.target.value } }),
3704
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded focus:outline-none focus:border-blue-500",
3705
+ placeholder: "Image URL"
3706
+ }
3707
+ ) }),
3708
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Fit", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3709
+ "select",
3710
+ {
3711
+ value: object.objectFit || "contain",
3712
+ onChange: (e) => onUpdate({
3713
+ object: { ...object, objectFit: e.target.value }
3714
+ }),
3715
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded",
3716
+ children: [
3717
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "contain", children: "Contain" }),
3718
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "cover", children: "Cover" }),
3719
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "fill", children: "Fill" }),
3720
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "none", children: "None" })
3721
+ ]
3722
+ }
3723
+ ) })
3724
+ ] });
3725
+ case "text":
3726
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(PropertySection, { title: "Text", children: [
3727
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Content", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3728
+ "textarea",
3729
+ {
3730
+ value: object.content,
3731
+ onChange: (e) => onUpdate({ object: { ...object, content: e.target.value } }),
3732
+ rows: 3,
3733
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded focus:outline-none focus:border-blue-500 resize-none"
3734
+ }
3735
+ ) }),
3736
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Size", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3737
+ "input",
3738
+ {
3739
+ type: "number",
3740
+ value: object.fontSize || 16,
3741
+ onChange: (e) => onUpdate({ object: { ...object, fontSize: parseInt(e.target.value) || 16 } }),
3742
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded"
3743
+ }
3744
+ ) }),
3745
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Color", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3746
+ "input",
3747
+ {
3748
+ type: "color",
3749
+ value: object.color || "#000000",
3750
+ onChange: (e) => onUpdate({ object: { ...object, color: e.target.value } }),
3751
+ className: "w-8 h-6 cursor-pointer"
3752
+ }
3753
+ ) }),
3754
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Align", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3755
+ "select",
3756
+ {
3757
+ value: object.textAlign || "left",
3758
+ onChange: (e) => onUpdate({
3759
+ object: { ...object, textAlign: e.target.value }
3760
+ }),
3761
+ className: "w-full px-2 py-1 text-sm border border-neutral-200 rounded",
3762
+ children: [
3763
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "left", children: "Left" }),
3764
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "center", children: "Center" }),
3765
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("option", { value: "right", children: "Right" })
3766
+ ]
3767
+ }
3768
+ ) })
3769
+ ] });
3770
+ case "shape":
3771
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(PropertySection, { title: "Shape", children: [
3772
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Fill", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3773
+ "input",
3774
+ {
3775
+ type: "color",
3776
+ value: object.fill || "#cccccc",
3777
+ onChange: (e) => onUpdate({ object: { ...object, fill: e.target.value } }),
3778
+ className: "w-8 h-6 cursor-pointer"
3779
+ }
3780
+ ) }),
3781
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PropertyRow, { label: "Stroke", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3782
+ "input",
3783
+ {
3784
+ type: "color",
3785
+ value: object.stroke || "#000000",
3786
+ onChange: (e) => onUpdate({ object: { ...object, stroke: e.target.value } }),
3787
+ className: "w-8 h-6 cursor-pointer"
3788
+ }
3789
+ ) })
3790
+ ] });
3791
+ default:
3792
+ return null;
3793
+ }
3794
+ }
3795
+
3796
+ // src/components/animation/AnimationEditor.tsx
3797
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3798
+ function AnimationEditor({
3799
+ composition: initialComposition,
3800
+ isOpen,
3801
+ onClose,
3802
+ onSave
3803
+ }) {
3804
+ const { state, dispatch, selectedLayers, flattenedLayers } = useComposition(initialComposition);
3805
+ const { seek } = usePlayback({
3806
+ duration: state.composition.duration,
3807
+ currentTime: state.playback.currentTime,
3808
+ isPlaying: state.playback.isPlaying,
3809
+ playbackRate: state.playback.playbackRate,
3810
+ audioTracks: state.composition.audioTracks,
3811
+ onTimeUpdate: (time) => dispatch({ type: "SEEK", payload: { time } }),
3812
+ onPlaybackEnd: () => dispatch({ type: "PAUSE" })
3813
+ });
3814
+ import_react18.default.useEffect(() => {
3815
+ const handleKeyDown = (e) => {
3816
+ if (e.key === "Escape" && !state.isDirty) {
3817
+ onClose();
3818
+ }
3819
+ if (e.key === " " && e.target === document.body) {
3820
+ e.preventDefault();
3821
+ dispatch({ type: state.playback.isPlaying ? "PAUSE" : "PLAY" });
3822
+ }
3823
+ };
3824
+ if (isOpen) {
3825
+ document.addEventListener("keydown", handleKeyDown);
3826
+ return () => document.removeEventListener("keydown", handleKeyDown);
3827
+ }
3828
+ }, [isOpen, state.isDirty, state.playback.isPlaying, dispatch, onClose]);
3829
+ if (!isOpen) return null;
3830
+ const handleSave = () => {
3831
+ onSave(state.composition);
3832
+ dispatch({ type: "MARK_SAVED" });
3833
+ };
3834
+ const handleAddFolder = () => {
3835
+ const newFolder = createLayerFolder(
3836
+ `folder-${Date.now()}`,
3837
+ `Folder ${state.composition.layers.length + 1}`
3838
+ );
3839
+ dispatch({ type: "ADD_LAYER", payload: { layer: newFolder } });
3840
+ };
3841
+ const handleAddItem = (type) => {
3842
+ let object;
3843
+ switch (type) {
3844
+ case "image":
3845
+ object = {
3846
+ id: `obj-${Date.now()}`,
3847
+ type: "image",
3848
+ src: "",
3849
+ position: { x: 50, y: 50 },
3850
+ size: { width: 50, height: 50, unit: "percent" },
3851
+ objectFit: "contain"
3852
+ };
3853
+ break;
3854
+ case "text":
3855
+ object = {
3856
+ id: `obj-${Date.now()}`,
3857
+ type: "text",
3858
+ content: "Text",
3859
+ position: { x: 50, y: 50 },
3860
+ fontSize: 24,
3861
+ color: "#000000",
3862
+ textAlign: "center"
3863
+ };
3864
+ break;
3865
+ case "shape":
3866
+ object = {
3867
+ id: `obj-${Date.now()}`,
3868
+ type: "shape",
3869
+ shape: "rectangle",
3870
+ position: { x: 50, y: 50 },
3871
+ size: { width: 20, height: 20, unit: "percent" },
3872
+ fill: "#cccccc"
3873
+ };
3874
+ break;
3875
+ }
3876
+ const newItem = createLayerItem(
3877
+ `layer-${Date.now()}`,
3878
+ `${type.charAt(0).toUpperCase() + type.slice(1)} ${flattenedLayers.length + 1}`,
3879
+ object
3880
+ );
3881
+ dispatch({ type: "ADD_LAYER", payload: { layer: newItem } });
3882
+ };
3883
+ const handleAddKeyframe = (layerId, property) => {
3884
+ const layer = findLayerById(state.composition.layers, layerId);
3885
+ if (!layer) return;
3886
+ const properties = {};
3887
+ if (property === "opacity" || !property) {
3888
+ properties.opacity = layer.opacity;
3889
+ }
3890
+ const keyframe = createKeyframe(state.playback.currentTime, properties);
3891
+ dispatch({ type: "ADD_KEYFRAME", payload: { layerId, keyframe } });
3892
+ };
3893
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "fixed inset-0 z-50 bg-neutral-900 flex flex-col", children: [
3894
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "h-12 bg-neutral-800 border-b border-neutral-700 flex items-center px-4", children: [
3895
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-3", children: [
3896
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "material-icons text-amber-500", children: "movie" }),
3897
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-white font-medium", children: state.composition.name }),
3898
+ state.isDirty && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-amber-400", children: "\u2022 Unsaved changes" })
3899
+ ] }),
3900
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex-1" }),
3901
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
3902
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-neutral-400 text-sm mr-4", children: [
3903
+ "Duration: ",
3904
+ formatTimecode(state.composition.duration)
3905
+ ] }),
3906
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3907
+ "button",
3908
+ {
3909
+ onClick: handleSave,
3910
+ disabled: !state.isDirty,
3911
+ className: "px-4 py-1.5 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed",
3912
+ children: "Save"
3913
+ }
3914
+ ),
3915
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3916
+ "button",
3917
+ {
3918
+ onClick: onClose,
3919
+ className: "p-1.5 hover:bg-neutral-700 rounded text-neutral-400 hover:text-white",
3920
+ title: "Close",
3921
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "material-icons", children: "close" })
3922
+ }
3923
+ )
3924
+ ] })
3925
+ ] }),
3926
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex-1 flex overflow-hidden", children: [
3927
+ state.panels.layers && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "w-[200px] flex-shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3928
+ LayerPanel,
3929
+ {
3930
+ layers: state.composition.layers,
3931
+ selectedIds: state.selection.layerIds,
3932
+ onSelect: (layerId, additive) => dispatch({ type: "SELECT_LAYERS", payload: { layerIds: [layerId], additive } }),
3933
+ onToggleVisibility: (layerId) => dispatch({ type: "TOGGLE_LAYER_VISIBILITY", payload: { layerId } }),
3934
+ onToggleLock: (layerId) => dispatch({ type: "TOGGLE_LAYER_LOCK", payload: { layerId } }),
3935
+ onToggleCollapsed: (folderId) => dispatch({ type: "TOGGLE_FOLDER_COLLAPSED", payload: { folderId } }),
3936
+ onAddFolder: handleAddFolder,
3937
+ onAddItem: handleAddItem,
3938
+ onDelete: (layerId) => dispatch({ type: "REMOVE_LAYER", payload: { layerId } }),
3939
+ onRename: (layerId, name) => dispatch({ type: "UPDATE_LAYER", payload: { layerId, updates: { name } } })
3940
+ }
3941
+ ) }),
3942
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
3943
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3944
+ PreviewCanvas,
3945
+ {
3946
+ composition: state.composition,
3947
+ currentTime: state.playback.currentTime,
3948
+ selectedLayerIds: state.selection.layerIds,
3949
+ onLayerSelect: (layerId) => dispatch({ type: "SELECT_LAYERS", payload: { layerIds: [layerId] } }),
3950
+ onClearSelection: () => dispatch({ type: "CLEAR_SELECTION" })
3951
+ }
3952
+ ),
3953
+ state.panels.timeline && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "h-[250px] flex-shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3954
+ Timeline,
3955
+ {
3956
+ composition: state.composition,
3957
+ currentTime: state.playback.currentTime,
3958
+ isPlaying: state.playback.isPlaying,
3959
+ zoom: state.timeline.zoom,
3960
+ scrollX: state.timeline.scrollX,
3961
+ selectedLayerIds: state.selection.layerIds,
3962
+ selectedKeyframeIds: state.selection.keyframeIds,
3963
+ snapToGrid: state.timeline.snapToGrid,
3964
+ gridSize: state.timeline.gridSize,
3965
+ onSeek: seek,
3966
+ onZoomChange: (zoom) => dispatch({ type: "SET_TIMELINE_ZOOM", payload: { zoom } }),
3967
+ onScrollChange: (scrollX) => dispatch({ type: "SET_TIMELINE_SCROLL", payload: { scrollX } }),
3968
+ onKeyframeSelect: (keyframeId, additive) => dispatch({ type: "SELECT_KEYFRAMES", payload: { keyframeIds: [keyframeId], additive } }),
3969
+ onKeyframeMove: (layerId, keyframeId, newTime) => dispatch({ type: "MOVE_KEYFRAME", payload: { layerId, keyframeId, newTime } }),
3970
+ onKeyframeAdd: (layerId, time) => {
3971
+ const keyframe = createKeyframe(time, { opacity: 1 });
3972
+ dispatch({ type: "ADD_KEYFRAME", payload: { layerId, keyframe } });
3973
+ },
3974
+ onKeyframeDelete: (layerId, keyframeId) => dispatch({ type: "REMOVE_KEYFRAME", payload: { layerId, keyframeId } }),
3975
+ onPlay: () => dispatch({ type: "PLAY" }),
3976
+ onPause: () => dispatch({ type: "PAUSE" }),
3977
+ onStop: () => dispatch({ type: "STOP" })
3978
+ }
3979
+ ) })
3980
+ ] }),
3981
+ state.panels.properties && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3982
+ PropertiesPanel,
3983
+ {
3984
+ selectedLayers,
3985
+ currentTime: state.playback.currentTime,
3986
+ onUpdateLayer: (layerId, updates) => dispatch({ type: "UPDATE_LAYER", payload: { layerId, updates } }),
3987
+ onAddKeyframe: handleAddKeyframe
3988
+ }
3989
+ )
3990
+ ] })
3991
+ ] });
3992
+ }
1415
3993
 
1416
3994
  // src/index.ts
1417
- var import_react5 = __toESM(require("react"));
3995
+ var import_react19 = __toESM(require("react"));
1418
3996
  function MarkdownContent({ url, filename }) {
1419
- const [content, setContent] = (0, import_react5.useState)("");
1420
- const [loading, setLoading] = (0, import_react5.useState)(true);
1421
- const [error, setError] = (0, import_react5.useState)(null);
1422
- (0, import_react5.useEffect)(() => {
3997
+ const [content, setContent] = (0, import_react19.useState)("");
3998
+ const [loading, setLoading] = (0, import_react19.useState)(true);
3999
+ const [error, setError] = (0, import_react19.useState)(null);
4000
+ (0, import_react19.useEffect)(() => {
1423
4001
  fetch(url).then((res) => {
1424
4002
  if (!res.ok) throw new Error("Failed to load");
1425
4003
  return res.text();
@@ -1463,31 +4041,31 @@ function MarkdownContent({ url, filename }) {
1463
4041
  margin: 0
1464
4042
  };
1465
4043
  if (loading) {
1466
- return import_react5.default.createElement(
4044
+ return import_react19.default.createElement(
1467
4045
  "div",
1468
4046
  { style: centeredStyle },
1469
- import_react5.default.createElement("span", { style: { color: "#9ca3af" } }, "Loading...")
4047
+ import_react19.default.createElement("span", { style: { color: "#9ca3af" } }, "Loading...")
1470
4048
  );
1471
4049
  }
1472
4050
  if (error) {
1473
- return import_react5.default.createElement(
4051
+ return import_react19.default.createElement(
1474
4052
  "div",
1475
4053
  { style: centeredStyle },
1476
- import_react5.default.createElement("span", { style: { color: "#ef4444" } }, "Failed to load markdown")
4054
+ import_react19.default.createElement("span", { style: { color: "#ef4444" } }, "Failed to load markdown")
1477
4055
  );
1478
4056
  }
1479
4057
  const children = [];
1480
4058
  if (filename) {
1481
4059
  children.push(
1482
- import_react5.default.createElement(
4060
+ import_react19.default.createElement(
1483
4061
  "div",
1484
4062
  { key: "header", style: headerStyle },
1485
- import_react5.default.createElement("span", { style: { color: "#0284c7", fontWeight: 500, fontSize: "14px" } }, "\u{1F4C4} " + filename)
4063
+ import_react19.default.createElement("span", { style: { color: "#0284c7", fontWeight: 500, fontSize: "14px" } }, "\u{1F4C4} " + filename)
1486
4064
  )
1487
4065
  );
1488
4066
  }
1489
- children.push(import_react5.default.createElement("pre", { key: "content", style: preStyle }, content));
1490
- return import_react5.default.createElement("div", { style: containerStyle }, ...children);
4067
+ children.push(import_react19.default.createElement("pre", { key: "content", style: preStyle }, content));
4068
+ return import_react19.default.createElement("div", { style: containerStyle }, ...children);
1491
4069
  }
1492
4070
  var createSlide = {
1493
4071
  /**
@@ -1540,7 +4118,7 @@ var createSlide = {
1540
4118
  return createSimpleSlide(id, [{
1541
4119
  id: `${id}-markdown`,
1542
4120
  type: "component",
1543
- render: () => import_react5.default.createElement(MarkdownContent, { url, filename: options?.filename }),
4121
+ render: () => import_react19.default.createElement(MarkdownContent, { url, filename: options?.filename }),
1544
4122
  position: { x: 0, y: 0 },
1545
4123
  size: { width: 100, height: 100, unit: "percent" }
1546
4124
  }], { thumbnail });
@@ -1553,7 +4131,7 @@ var createSlide = {
1553
4131
  return createSimpleSlide(id, [{
1554
4132
  id: `${id}-audio`,
1555
4133
  type: "component",
1556
- render: () => import_react5.default.createElement(AudioContent, { url, filename: options?.filename }),
4134
+ render: () => import_react19.default.createElement(AudioContent, { url, filename: options?.filename }),
1557
4135
  position: { x: 0, y: 0 },
1558
4136
  size: { width: 100, height: 100, unit: "percent" }
1559
4137
  }], { thumbnail });
@@ -1588,27 +4166,45 @@ function AudioContent({ url, filename }) {
1588
4166
  marginBottom: "16px"
1589
4167
  };
1590
4168
  const children = [
1591
- import_react5.default.createElement(
4169
+ import_react19.default.createElement(
1592
4170
  "svg",
1593
4171
  { key: "icon", style: iconStyle, viewBox: "0 0 24 24", fill: "currentColor" },
1594
- import_react5.default.createElement("path", { d: "M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z" })
4172
+ import_react19.default.createElement("path", { d: "M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z" })
1595
4173
  )
1596
4174
  ];
1597
4175
  if (filename) {
1598
- children.push(import_react5.default.createElement("span", { key: "filename", style: filenameStyle }, filename));
4176
+ children.push(import_react19.default.createElement("span", { key: "filename", style: filenameStyle }, filename));
1599
4177
  }
1600
4178
  children.push(
1601
- import_react5.default.createElement("audio", { key: "audio", src: url, controls: true, style: { width: "100%", maxWidth: "280px" } })
4179
+ import_react19.default.createElement("audio", { key: "audio", src: url, controls: true, style: { width: "100%", maxWidth: "280px" } })
1602
4180
  );
1603
- return import_react5.default.createElement("div", { style: containerStyle }, ...children);
4181
+ return import_react19.default.createElement("div", { style: containerStyle }, ...children);
1604
4182
  }
1605
4183
  // Annotate the CommonJS export names for ESM import in node:
1606
4184
  0 && (module.exports = {
4185
+ AnimationEditor,
1607
4186
  Carousel,
4187
+ LayerPanel,
4188
+ PreviewCanvas,
4189
+ PropertiesPanel,
1608
4190
  SlideRenderer,
1609
4191
  Thumbnails,
4192
+ Timeline,
4193
+ createComposition,
1610
4194
  createImageSlide,
4195
+ createKeyframe,
4196
+ createLayerFolder,
4197
+ createLayerItem,
1611
4198
  createSimpleSlide,
1612
4199
  createSlide,
1613
- useCarousel
4200
+ findLayerById,
4201
+ flattenLayers,
4202
+ formatTimecode,
4203
+ getLayerPropertiesAtTime,
4204
+ interpolateKeyframes,
4205
+ parseTimecode,
4206
+ useCarousel,
4207
+ useComposition,
4208
+ useKeyframes,
4209
+ usePlayback
1614
4210
  });