@qwanyx/carousel 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1371,6 +1371,9 @@ var Carousel = forwardRef(
1371
1371
  );
1372
1372
  Carousel.displayName = "Carousel";
1373
1373
 
1374
+ // src/components/animation/AnimationEditor.tsx
1375
+ import React14 from "react";
1376
+
1374
1377
  // src/types.ts
1375
1378
  function createSimpleSlide(id, objects, options) {
1376
1379
  return {
@@ -1411,14 +1414,2537 @@ function createImageSlide(id, src, options) {
1411
1414
  }
1412
1415
  return createSimpleSlide(id, objects, { background: options?.background });
1413
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
+ }
1414
1474
 
1415
- // src/index.ts
1416
- import React4, { useState as useState4, useEffect as useEffect4 } from "react";
1417
- function MarkdownContent({ url, filename }) {
1418
- const [content, setContent] = useState4("");
1419
- const [loading, setLoading] = useState4(true);
1420
- 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
+ );
1421
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 group
2014
+ ${isSelected ? "bg-[#4a6fa5]/40" : "hover:bg-[#3a3a3a]"}
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-500 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-[#1a1a1a] text-neutral-200 border border-blue-500 rounded outline-none",
2032
+ onClick: (e) => e.stopPropagation()
2033
+ }
2034
+ ) : /* @__PURE__ */ jsx4("span", { className: "flex-1 text-sm text-neutral-300 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-[#4a4a4a] rounded",
2044
+ title: layer.visible ? "Hide" : "Show",
2045
+ children: /* @__PURE__ */ jsx4("span", { className: "material-icons text-neutral-500", 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-[#4a4a4a] rounded",
2056
+ title: layer.locked ? "Unlock" : "Lock",
2057
+ children: /* @__PURE__ */ jsx4("span", { className: "material-icons text-neutral-500", 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-900/50 rounded",
2068
+ title: "Delete",
2069
+ children: /* @__PURE__ */ jsx4("span", { className: "material-icons text-neutral-500 hover:text-red-400", 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-[#4a6fa5]/40" : "hover:bg-[#3a3a3a]"}
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-[#4a4a4a] rounded mr-1",
2145
+ children: /* @__PURE__ */ jsx5(
2146
+ "span",
2147
+ {
2148
+ className: "material-icons text-neutral-500 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-[#1a1a1a] text-neutral-200 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-200 truncate", children: folder.name }),
2172
+ /* @__PURE__ */ jsxs5("span", { className: "text-xs text-neutral-500 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-[#4a4a4a] rounded",
2186
+ title: folder.visible ? "Hide" : "Show",
2187
+ children: /* @__PURE__ */ jsx5("span", { className: "material-icons text-neutral-500", 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-[#4a4a4a] rounded",
2198
+ title: folder.locked ? "Unlock" : "Lock",
2199
+ children: /* @__PURE__ */ jsx5("span", { className: "material-icons text-neutral-500", 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-900/50 rounded",
2210
+ title: "Delete folder",
2211
+ children: /* @__PURE__ */ jsx5("span", { className: "material-icons text-neutral-500 hover:text-red-400", 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-[#232323] border-r border-[#3a3a3a]", children: [
2272
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between px-3 py-2 border-b border-[#3a3a3a]", children: [
2273
+ /* @__PURE__ */ jsx6("h3", { className: "text-sm font-semibold text-neutral-300", 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-[#3a3a3a] rounded",
2280
+ title: "Add layer",
2281
+ children: /* @__PURE__ */ jsx6("span", { className: "material-icons text-neutral-400", 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-[#2a2a2a] rounded-lg shadow-lg border border-[#3a3a3a] 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 text-neutral-300 hover:bg-[#3a3a3a] 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-[#3a3a3a] 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 text-neutral-300 hover:bg-[#3a3a3a] flex items-center gap-2",
2316
+ children: [
2317
+ /* @__PURE__ */ jsx6("span", { className: "material-icons text-blue-400", 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 text-neutral-300 hover:bg-[#3a3a3a] flex items-center gap-2",
2330
+ children: [
2331
+ /* @__PURE__ */ jsx6("span", { className: "material-icons text-green-400", 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 text-neutral-300 hover:bg-[#3a3a3a] flex items-center gap-2",
2344
+ children: [
2345
+ /* @__PURE__ */ jsx6("span", { className: "material-icons text-purple-400", 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-500 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-[#3a3a3a] bg-[#1a1a1a]", children: /* @__PURE__ */ jsxs6("p", { className: "text-xs text-neutral-400", 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-[#1a1a1a] border-b border-[#3a3a3a] 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-500" : "h-2 bg-neutral-600"}`,
2433
+ style: { marginTop: isMajor ? 0 : 8 }
2434
+ }
2435
+ ),
2436
+ isMajor && /* @__PURE__ */ jsx7(
2437
+ "span",
2438
+ {
2439
+ className: "absolute text-[10px] text-neutral-400 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-[#2a2a2a]
2612
+ ${isLayerSelected ? "bg-[#4a6fa5]/20" : "hover:bg-[#2a2a2a]"}
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-900/30" : "bg-[#2a2a2a]"}
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-[#1a1a1a] border-t border-[#3a3a3a]", children: [
2835
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-[#3a3a3a] bg-[#232323]", 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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-[#0a0a0a] text-blue-400 font-mono text-sm rounded border border-[#3a3a3a]", 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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-[#3a3a3a] rounded text-neutral-400 hover:text-white",
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-900/50 text-blue-400" : "hover:bg-[#3a3a3a] text-neutral-400"}`,
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-[#3a3a3a] overflow-y-auto bg-[#232323]", children: [
2899
+ /* @__PURE__ */ jsx12("div", { className: "h-6 bg-[#1a1a1a] border-b border-[#3a3a3a] 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-[#2a2a2a] px-2 flex items-center
2907
+ ${selectedLayerIds.includes(layer.id) ? "bg-[#4a6fa5]/30" : ""}
2908
+ ${!layer.visible ? "opacity-50" : ""}
2909
+ `,
2910
+ style: { paddingLeft: depth * 12 + 8 },
2911
+ children: [
2912
+ /* @__PURE__ */ jsx12("span", { className: "material-icons text-neutral-500 mr-1", style: { fontSize: 12 }, children: layer.type === "folder" ? "folder" : "lens" }),
2913
+ /* @__PURE__ */ jsx12("span", { className: "text-xs text-neutral-300 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-[#2a2a2a] px-2 flex items-center",
2923
+ children: [
2924
+ /* @__PURE__ */ jsx12("span", { className: "material-icons text-blue-400 mr-1", style: { fontSize: 12 }, children: "audiotrack" }),
2925
+ /* @__PURE__ */ jsx12("span", { className: "text-xs text-neutral-300 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 bg-[#1e1e1e]",
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-[#232323] border-l border-[#3a3a3a] flex flex-col", children: [
3483
+ /* @__PURE__ */ jsx15("div", { className: "px-3 py-2 border-b border-[#3a3a3a]", children: /* @__PURE__ */ jsx15("h3", { className: "text-sm font-semibold text-neutral-300", children: "Properties" }) }),
3484
+ /* @__PURE__ */ jsx15("div", { className: "flex-1 flex items-center justify-center text-neutral-500 text-sm p-4 text-center", children: "Select a layer to edit its properties" })
3485
+ ] });
3486
+ }
3487
+ if (selectedLayers.length > 1) {
3488
+ return /* @__PURE__ */ jsxs13("div", { className: "w-[250px] flex-shrink-0 bg-[#232323] border-l border-[#3a3a3a] flex flex-col", children: [
3489
+ /* @__PURE__ */ jsxs13("div", { className: "px-3 py-2 border-b border-[#3a3a3a]", children: [
3490
+ /* @__PURE__ */ jsx15("h3", { className: "text-sm font-semibold text-neutral-300", children: "Properties" }),
3491
+ /* @__PURE__ */ jsxs13("p", { className: "text-xs text-neutral-500", 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-[#232323] border-l border-[#3a3a3a] flex flex-col", children: [
3501
+ /* @__PURE__ */ jsxs13("div", { className: "px-3 py-2 border-b border-[#3a3a3a]", children: [
3502
+ /* @__PURE__ */ jsx15("h3", { className: "text-sm font-semibold text-neutral-300", children: "Properties" }),
3503
+ /* @__PURE__ */ jsx15("p", { className: "text-xs text-neutral-500 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 accent-blue-500"
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 accent-blue-500"
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 accent-blue-500"
3545
+ }
3546
+ ),
3547
+ /* @__PURE__ */ jsxs13("span", { className: "text-xs text-neutral-400 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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-[#1a1a1a] rounded flex justify-between text-neutral-300",
3586
+ children: [
3587
+ /* @__PURE__ */ jsx15("span", { children: formatTimecode(kf.time) }),
3588
+ /* @__PURE__ */ jsx15("span", { className: "text-neutral-500", 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-[#3a3a3a]", 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-[#2a2a2a]",
3605
+ children: [
3606
+ /* @__PURE__ */ jsx15("span", { className: "text-xs font-semibold text-neutral-400 uppercase", children: title }),
3607
+ /* @__PURE__ */ jsx15("span", { className: "material-icons text-neutral-500", 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-900/50 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-transparent"
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 bg-[#1a1a1a] text-neutral-200 border border-[#3a3a3a] 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 bg-transparent"
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 bg-transparent"
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(() => {
1422
3948
  fetch(url).then((res) => {
1423
3949
  if (!res.ok) throw new Error("Failed to load");
1424
3950
  return res.text();
@@ -1462,31 +3988,31 @@ function MarkdownContent({ url, filename }) {
1462
3988
  margin: 0
1463
3989
  };
1464
3990
  if (loading) {
1465
- return React4.createElement(
3991
+ return React15.createElement(
1466
3992
  "div",
1467
3993
  { style: centeredStyle },
1468
- React4.createElement("span", { style: { color: "#9ca3af" } }, "Loading...")
3994
+ React15.createElement("span", { style: { color: "#9ca3af" } }, "Loading...")
1469
3995
  );
1470
3996
  }
1471
3997
  if (error) {
1472
- return React4.createElement(
3998
+ return React15.createElement(
1473
3999
  "div",
1474
4000
  { style: centeredStyle },
1475
- React4.createElement("span", { style: { color: "#ef4444" } }, "Failed to load markdown")
4001
+ React15.createElement("span", { style: { color: "#ef4444" } }, "Failed to load markdown")
1476
4002
  );
1477
4003
  }
1478
4004
  const children = [];
1479
4005
  if (filename) {
1480
4006
  children.push(
1481
- React4.createElement(
4007
+ React15.createElement(
1482
4008
  "div",
1483
4009
  { key: "header", style: headerStyle },
1484
- 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)
1485
4011
  )
1486
4012
  );
1487
4013
  }
1488
- children.push(React4.createElement("pre", { key: "content", style: preStyle }, content));
1489
- 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);
1490
4016
  }
1491
4017
  var createSlide = {
1492
4018
  /**
@@ -1539,7 +4065,7 @@ var createSlide = {
1539
4065
  return createSimpleSlide(id, [{
1540
4066
  id: `${id}-markdown`,
1541
4067
  type: "component",
1542
- render: () => React4.createElement(MarkdownContent, { url, filename: options?.filename }),
4068
+ render: () => React15.createElement(MarkdownContent, { url, filename: options?.filename }),
1543
4069
  position: { x: 0, y: 0 },
1544
4070
  size: { width: 100, height: 100, unit: "percent" }
1545
4071
  }], { thumbnail });
@@ -1552,7 +4078,7 @@ var createSlide = {
1552
4078
  return createSimpleSlide(id, [{
1553
4079
  id: `${id}-audio`,
1554
4080
  type: "component",
1555
- render: () => React4.createElement(AudioContent, { url, filename: options?.filename }),
4081
+ render: () => React15.createElement(AudioContent, { url, filename: options?.filename }),
1556
4082
  position: { x: 0, y: 0 },
1557
4083
  size: { width: 100, height: 100, unit: "percent" }
1558
4084
  }], { thumbnail });
@@ -1587,26 +4113,44 @@ function AudioContent({ url, filename }) {
1587
4113
  marginBottom: "16px"
1588
4114
  };
1589
4115
  const children = [
1590
- React4.createElement(
4116
+ React15.createElement(
1591
4117
  "svg",
1592
4118
  { key: "icon", style: iconStyle, viewBox: "0 0 24 24", fill: "currentColor" },
1593
- 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" })
1594
4120
  )
1595
4121
  ];
1596
4122
  if (filename) {
1597
- children.push(React4.createElement("span", { key: "filename", style: filenameStyle }, filename));
4123
+ children.push(React15.createElement("span", { key: "filename", style: filenameStyle }, filename));
1598
4124
  }
1599
4125
  children.push(
1600
- 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" } })
1601
4127
  );
1602
- return React4.createElement("div", { style: containerStyle }, ...children);
4128
+ return React15.createElement("div", { style: containerStyle }, ...children);
1603
4129
  }
1604
4130
  export {
4131
+ AnimationEditor,
1605
4132
  Carousel,
4133
+ LayerPanel,
4134
+ PreviewCanvas,
4135
+ PropertiesPanel,
1606
4136
  SlideRenderer,
1607
4137
  Thumbnails,
4138
+ Timeline,
4139
+ createComposition,
1608
4140
  createImageSlide,
4141
+ createKeyframe,
4142
+ createLayerFolder,
4143
+ createLayerItem,
1609
4144
  createSimpleSlide,
1610
4145
  createSlide,
1611
- useCarousel
4146
+ findLayerById,
4147
+ flattenLayers,
4148
+ formatTimecode,
4149
+ getLayerPropertiesAtTime,
4150
+ interpolateKeyframes,
4151
+ parseTimecode,
4152
+ useCarousel,
4153
+ useComposition,
4154
+ useKeyframes,
4155
+ usePlayback
1612
4156
  };