@qwanyx/carousel 0.1.6 → 0.1.8

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