@qwanyx/carousel 0.1.7 → 0.1.9

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.
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
 
@@ -1406,6 +1424,9 @@ var Carousel = (0, import_react4.forwardRef)(
1406
1424
  );
1407
1425
  Carousel.displayName = "Carousel";
1408
1426
 
1427
+ // src/components/animation/AnimationEditor.tsx
1428
+ var import_react18 = __toESM(require("react"));
1429
+
1409
1430
  // src/types.ts
1410
1431
  function createSimpleSlide(id, objects, options) {
1411
1432
  return {
@@ -1446,14 +1467,2537 @@ function createImageSlide(id, src, options) {
1446
1467
  }
1447
1468
  return createSimpleSlide(id, objects, { background: options?.background });
1448
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 group
2067
+ ${isSelected ? "bg-[#4a6fa5]/40" : "hover:bg-[#3a3a3a]"}
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-500 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-[#1a1a1a] text-neutral-200 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-300 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-[#4a4a4a] rounded",
2097
+ title: layer.visible ? "Hide" : "Show",
2098
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-500", 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-[#4a4a4a] rounded",
2109
+ title: layer.locked ? "Unlock" : "Lock",
2110
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-500", 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-900/50 rounded",
2121
+ title: "Delete",
2122
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "material-icons text-neutral-500 hover:text-red-400", 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-[#4a6fa5]/40" : "hover:bg-[#3a3a3a]"}
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-[#4a4a4a] rounded mr-1",
2198
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2199
+ "span",
2200
+ {
2201
+ className: "material-icons text-neutral-500 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-[#1a1a1a] text-neutral-200 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-200 truncate", children: folder.name }),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "text-xs text-neutral-500 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-[#4a4a4a] rounded",
2239
+ title: folder.visible ? "Hide" : "Show",
2240
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-neutral-500", 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-[#4a4a4a] rounded",
2251
+ title: folder.locked ? "Unlock" : "Lock",
2252
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-neutral-500", 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-900/50 rounded",
2263
+ title: "Delete folder",
2264
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "material-icons text-neutral-500 hover:text-red-400", 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-[#232323] border-r border-[#3a3a3a]", children: [
2325
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-[#3a3a3a]", children: [
2326
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-sm font-semibold text-neutral-300", 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-[#3a3a3a] rounded",
2333
+ title: "Add layer",
2334
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-neutral-400", 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-[#2a2a2a] rounded-lg shadow-lg border border-[#3a3a3a] 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 text-neutral-300 hover:bg-[#3a3a3a] 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-[#3a3a3a] 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 text-neutral-300 hover:bg-[#3a3a3a] flex items-center gap-2",
2369
+ children: [
2370
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-blue-400", 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 text-neutral-300 hover:bg-[#3a3a3a] flex items-center gap-2",
2383
+ children: [
2384
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-green-400", 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 text-neutral-300 hover:bg-[#3a3a3a] flex items-center gap-2",
2397
+ children: [
2398
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "material-icons text-purple-400", 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-500 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-[#3a3a3a] bg-[#1a1a1a]", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-neutral-400", 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-[#1a1a1a] border-b border-[#3a3a3a] 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-500" : "h-2 bg-neutral-600"}`,
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-400 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-[#2a2a2a]
2665
+ ${isLayerSelected ? "bg-[#4a6fa5]/20" : "hover:bg-[#2a2a2a]"}
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-900/30" : "bg-[#2a2a2a]"}
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-[#1a1a1a] border-t border-[#3a3a3a]", children: [
2888
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-[#3a3a3a] bg-[#232323]", 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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-[#0a0a0a] text-blue-400 font-mono text-sm rounded border border-[#3a3a3a]", 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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-900/50 text-blue-400" : "hover:bg-[#3a3a3a] text-neutral-400"}`,
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-[#3a3a3a] overflow-y-auto bg-[#232323]", children: [
2952
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "h-6 bg-[#1a1a1a] border-b border-[#3a3a3a] 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-[#2a2a2a] px-2 flex items-center
2960
+ ${selectedLayerIds.includes(layer.id) ? "bg-[#4a6fa5]/30" : ""}
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-500 mr-1", style: { fontSize: 12 }, children: layer.type === "folder" ? "folder" : "lens" }),
2966
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-neutral-300 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-[#2a2a2a] px-2 flex items-center",
2976
+ children: [
2977
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "material-icons text-blue-400 mr-1", style: { fontSize: 12 }, children: "audiotrack" }),
2978
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-neutral-300 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 bg-[#1e1e1e]",
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-[#232323] border-l border-[#3a3a3a] flex flex-col", children: [
3536
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "px-3 py-2 border-b border-[#3a3a3a]", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-semibold text-neutral-300", children: "Properties" }) }),
3537
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex-1 flex items-center justify-center text-neutral-500 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-[#232323] border-l border-[#3a3a3a] flex flex-col", children: [
3542
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "px-3 py-2 border-b border-[#3a3a3a]", children: [
3543
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-semibold text-neutral-300", children: "Properties" }),
3544
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-xs text-neutral-500", 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-[#232323] border-l border-[#3a3a3a] flex flex-col", children: [
3554
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "px-3 py-2 border-b border-[#3a3a3a]", children: [
3555
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-semibold text-neutral-300", children: "Properties" }),
3556
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-xs text-neutral-500 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 accent-blue-500"
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 accent-blue-500"
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 accent-blue-500"
3598
+ }
3599
+ ),
3600
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-xs text-neutral-400 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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-[#1a1a1a] rounded flex justify-between text-neutral-300",
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-500", 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-[#3a3a3a]", 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-[#2a2a2a]",
3658
+ children: [
3659
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-xs font-semibold text-neutral-400 uppercase", children: title }),
3660
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-icons text-neutral-500", 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-900/50 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-transparent"
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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-transparent"
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 bg-transparent"
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
+ }
1449
3993
 
1450
3994
  // src/index.ts
1451
- var import_react5 = __toESM(require("react"));
3995
+ var import_react19 = __toESM(require("react"));
1452
3996
  function MarkdownContent({ url, filename }) {
1453
- const [content, setContent] = (0, import_react5.useState)("");
1454
- const [loading, setLoading] = (0, import_react5.useState)(true);
1455
- const [error, setError] = (0, import_react5.useState)(null);
1456
- (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)(() => {
1457
4001
  fetch(url).then((res) => {
1458
4002
  if (!res.ok) throw new Error("Failed to load");
1459
4003
  return res.text();
@@ -1497,31 +4041,31 @@ function MarkdownContent({ url, filename }) {
1497
4041
  margin: 0
1498
4042
  };
1499
4043
  if (loading) {
1500
- return import_react5.default.createElement(
4044
+ return import_react19.default.createElement(
1501
4045
  "div",
1502
4046
  { style: centeredStyle },
1503
- import_react5.default.createElement("span", { style: { color: "#9ca3af" } }, "Loading...")
4047
+ import_react19.default.createElement("span", { style: { color: "#9ca3af" } }, "Loading...")
1504
4048
  );
1505
4049
  }
1506
4050
  if (error) {
1507
- return import_react5.default.createElement(
4051
+ return import_react19.default.createElement(
1508
4052
  "div",
1509
4053
  { style: centeredStyle },
1510
- 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")
1511
4055
  );
1512
4056
  }
1513
4057
  const children = [];
1514
4058
  if (filename) {
1515
4059
  children.push(
1516
- import_react5.default.createElement(
4060
+ import_react19.default.createElement(
1517
4061
  "div",
1518
4062
  { key: "header", style: headerStyle },
1519
- 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)
1520
4064
  )
1521
4065
  );
1522
4066
  }
1523
- children.push(import_react5.default.createElement("pre", { key: "content", style: preStyle }, content));
1524
- 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);
1525
4069
  }
1526
4070
  var createSlide = {
1527
4071
  /**
@@ -1574,7 +4118,7 @@ var createSlide = {
1574
4118
  return createSimpleSlide(id, [{
1575
4119
  id: `${id}-markdown`,
1576
4120
  type: "component",
1577
- render: () => import_react5.default.createElement(MarkdownContent, { url, filename: options?.filename }),
4121
+ render: () => import_react19.default.createElement(MarkdownContent, { url, filename: options?.filename }),
1578
4122
  position: { x: 0, y: 0 },
1579
4123
  size: { width: 100, height: 100, unit: "percent" }
1580
4124
  }], { thumbnail });
@@ -1587,7 +4131,7 @@ var createSlide = {
1587
4131
  return createSimpleSlide(id, [{
1588
4132
  id: `${id}-audio`,
1589
4133
  type: "component",
1590
- render: () => import_react5.default.createElement(AudioContent, { url, filename: options?.filename }),
4134
+ render: () => import_react19.default.createElement(AudioContent, { url, filename: options?.filename }),
1591
4135
  position: { x: 0, y: 0 },
1592
4136
  size: { width: 100, height: 100, unit: "percent" }
1593
4137
  }], { thumbnail });
@@ -1622,27 +4166,45 @@ function AudioContent({ url, filename }) {
1622
4166
  marginBottom: "16px"
1623
4167
  };
1624
4168
  const children = [
1625
- import_react5.default.createElement(
4169
+ import_react19.default.createElement(
1626
4170
  "svg",
1627
4171
  { key: "icon", style: iconStyle, viewBox: "0 0 24 24", fill: "currentColor" },
1628
- 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" })
1629
4173
  )
1630
4174
  ];
1631
4175
  if (filename) {
1632
- 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));
1633
4177
  }
1634
4178
  children.push(
1635
- 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" } })
1636
4180
  );
1637
- return import_react5.default.createElement("div", { style: containerStyle }, ...children);
4181
+ return import_react19.default.createElement("div", { style: containerStyle }, ...children);
1638
4182
  }
1639
4183
  // Annotate the CommonJS export names for ESM import in node:
1640
4184
  0 && (module.exports = {
4185
+ AnimationEditor,
1641
4186
  Carousel,
4187
+ LayerPanel,
4188
+ PreviewCanvas,
4189
+ PropertiesPanel,
1642
4190
  SlideRenderer,
1643
4191
  Thumbnails,
4192
+ Timeline,
4193
+ createComposition,
1644
4194
  createImageSlide,
4195
+ createKeyframe,
4196
+ createLayerFolder,
4197
+ createLayerItem,
1645
4198
  createSimpleSlide,
1646
4199
  createSlide,
1647
- useCarousel
4200
+ findLayerById,
4201
+ flattenLayers,
4202
+ formatTimecode,
4203
+ getLayerPropertiesAtTime,
4204
+ interpolateKeyframes,
4205
+ parseTimecode,
4206
+ useCarousel,
4207
+ useComposition,
4208
+ useKeyframes,
4209
+ usePlayback
1648
4210
  });