@dawcore/components 0.0.1 → 0.0.3

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
@@ -551,7 +551,7 @@ DawTransportElement = __decorateClass([
551
551
 
552
552
  // src/elements/daw-play-button.ts
553
553
  import { html as html3 } from "lit";
554
- import { customElement as customElement6 } from "lit/decorators.js";
554
+ import { customElement as customElement6, state } from "lit/decorators.js";
555
555
 
556
556
  // src/elements/daw-transport-button.ts
557
557
  import { LitElement as LitElement6, css as css3 } from "lit";
@@ -581,9 +581,40 @@ DawTransportButton.styles = css3`
581
581
 
582
582
  // src/elements/daw-play-button.ts
583
583
  var DawPlayButtonElement = class extends DawTransportButton {
584
+ constructor() {
585
+ super(...arguments);
586
+ this._isRecording = false;
587
+ this._targetRef = null;
588
+ this._onRecStart = () => {
589
+ this._isRecording = true;
590
+ };
591
+ this._onRecEnd = () => {
592
+ this._isRecording = false;
593
+ };
594
+ }
595
+ connectedCallback() {
596
+ super.connectedCallback();
597
+ requestAnimationFrame(() => {
598
+ const target = this.target;
599
+ if (!target) return;
600
+ this._targetRef = target;
601
+ target.addEventListener("daw-recording-start", this._onRecStart);
602
+ target.addEventListener("daw-recording-complete", this._onRecEnd);
603
+ target.addEventListener("daw-recording-error", this._onRecEnd);
604
+ });
605
+ }
606
+ disconnectedCallback() {
607
+ super.disconnectedCallback();
608
+ if (this._targetRef) {
609
+ this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
610
+ this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
611
+ this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
612
+ this._targetRef = null;
613
+ }
614
+ }
584
615
  render() {
585
616
  return html3`
586
- <button part="button" @click=${this._onClick}>
617
+ <button part="button" ?disabled=${this._isRecording} @click=${this._onClick}>
587
618
  <slot>Play</slot>
588
619
  </button>
589
620
  `;
@@ -599,17 +630,53 @@ var DawPlayButtonElement = class extends DawTransportButton {
599
630
  target.play();
600
631
  }
601
632
  };
633
+ __decorateClass([
634
+ state()
635
+ ], DawPlayButtonElement.prototype, "_isRecording", 2);
602
636
  DawPlayButtonElement = __decorateClass([
603
637
  customElement6("daw-play-button")
604
638
  ], DawPlayButtonElement);
605
639
 
606
640
  // src/elements/daw-pause-button.ts
607
- import { html as html4 } from "lit";
608
- import { customElement as customElement7 } from "lit/decorators.js";
641
+ import { html as html4, css as css4 } from "lit";
642
+ import { customElement as customElement7, state as state2 } from "lit/decorators.js";
609
643
  var DawPauseButtonElement = class extends DawTransportButton {
644
+ constructor() {
645
+ super(...arguments);
646
+ this._isPaused = false;
647
+ this._isRecording = false;
648
+ this._targetRef = null;
649
+ this._onRecStart = () => {
650
+ this._isRecording = true;
651
+ };
652
+ this._onRecEnd = () => {
653
+ this._isRecording = false;
654
+ this._isPaused = false;
655
+ };
656
+ }
657
+ connectedCallback() {
658
+ super.connectedCallback();
659
+ requestAnimationFrame(() => {
660
+ const target = this.target;
661
+ if (!target) return;
662
+ this._targetRef = target;
663
+ target.addEventListener("daw-recording-start", this._onRecStart);
664
+ target.addEventListener("daw-recording-complete", this._onRecEnd);
665
+ target.addEventListener("daw-recording-error", this._onRecEnd);
666
+ });
667
+ }
668
+ disconnectedCallback() {
669
+ super.disconnectedCallback();
670
+ if (this._targetRef) {
671
+ this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
672
+ this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
673
+ this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
674
+ this._targetRef = null;
675
+ }
676
+ }
610
677
  render() {
611
678
  return html4`
612
- <button part="button" @click=${this._onClick}>
679
+ <button part="button" ?data-paused=${this._isPaused} @click=${this._onClick}>
613
680
  <slot>Pause</slot>
614
681
  </button>
615
682
  `;
@@ -622,9 +689,36 @@ var DawPauseButtonElement = class extends DawTransportButton {
622
689
  );
623
690
  return;
624
691
  }
625
- target.pause();
692
+ if (this._isRecording) {
693
+ if (this._isPaused) {
694
+ target.resumeRecording();
695
+ target.play(target.currentTime);
696
+ this._isPaused = false;
697
+ } else {
698
+ target.pauseRecording();
699
+ target.pause();
700
+ this._isPaused = true;
701
+ }
702
+ } else {
703
+ target.pause();
704
+ }
626
705
  }
627
706
  };
707
+ DawPauseButtonElement.styles = [
708
+ DawTransportButton.styles,
709
+ css4`
710
+ button[data-paused] {
711
+ background: rgba(255, 255, 255, 0.1);
712
+ border-color: var(--daw-controls-text, #e0d4c8);
713
+ }
714
+ `
715
+ ];
716
+ __decorateClass([
717
+ state2()
718
+ ], DawPauseButtonElement.prototype, "_isPaused", 2);
719
+ __decorateClass([
720
+ state2()
721
+ ], DawPauseButtonElement.prototype, "_isRecording", 2);
628
722
  DawPauseButtonElement = __decorateClass([
629
723
  customElement7("daw-pause-button")
630
724
  ], DawPauseButtonElement);
@@ -648,6 +742,9 @@ var DawStopButtonElement = class extends DawTransportButton {
648
742
  );
649
743
  return;
650
744
  }
745
+ if (target.isRecording) {
746
+ target.stopRecording();
747
+ }
651
748
  target.stop();
652
749
  }
653
750
  };
@@ -656,8 +753,8 @@ DawStopButtonElement = __decorateClass([
656
753
  ], DawStopButtonElement);
657
754
 
658
755
  // src/elements/daw-editor.ts
659
- import { LitElement as LitElement8, html as html7, css as css6 } from "lit";
660
- import { customElement as customElement10, property as property6, state } from "lit/decorators.js";
756
+ import { LitElement as LitElement8, html as html7, css as css7 } from "lit";
757
+ import { customElement as customElement10, property as property6, state as state3 } from "lit/decorators.js";
661
758
  import { createClipFromSeconds as createClipFromSeconds2, createTrack as createTrack2, clipPixelWidth } from "@waveform-playlist/core";
662
759
 
663
760
  // src/workers/peaksWorker.ts
@@ -979,20 +1076,22 @@ function extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, dura
979
1076
 
980
1077
  // src/workers/peakPipeline.ts
981
1078
  var PeakPipeline = class {
982
- constructor() {
1079
+ constructor(baseScale = 128, bits = 16) {
983
1080
  this._worker = null;
984
1081
  this._cache = /* @__PURE__ */ new WeakMap();
985
1082
  this._inflight = /* @__PURE__ */ new WeakMap();
1083
+ this._baseScale = baseScale;
1084
+ this._bits = bits;
986
1085
  }
987
1086
  /**
988
1087
  * Generate PeakData for a clip from its AudioBuffer.
989
1088
  * Uses cached WaveformData when available; otherwise generates via worker.
990
- * The worker generates at `scale` (= samplesPerPixel) for exact rendering.
1089
+ * Worker generates at baseScale (default 128); extractPeaks resamples to the requested zoom.
991
1090
  */
992
- async generatePeaks(audioBuffer, samplesPerPixel, isMono) {
993
- const waveformData = await this._getWaveformData(audioBuffer, samplesPerPixel);
1091
+ async generatePeaks(audioBuffer, samplesPerPixel, isMono, offsetSamples, durationSamples) {
1092
+ const waveformData = await this._getWaveformData(audioBuffer);
994
1093
  try {
995
- return extractPeaks(waveformData, samplesPerPixel, isMono);
1094
+ return extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, durationSamples);
996
1095
  } catch (err) {
997
1096
  console.warn("[dawcore] extractPeaks failed: " + String(err));
998
1097
  throw err;
@@ -1004,14 +1103,24 @@ var PeakPipeline = class {
1004
1103
  * Returns a new Map of clipId → PeakData. Clips without cached data or where
1005
1104
  * the target scale is finer than the cached base are skipped.
1006
1105
  */
1007
- reextractPeaks(clipBuffers, samplesPerPixel, isMono) {
1106
+ reextractPeaks(clipBuffers, samplesPerPixel, isMono, clipOffsets) {
1008
1107
  const result = /* @__PURE__ */ new Map();
1009
1108
  for (const [clipId, audioBuffer] of clipBuffers) {
1010
1109
  const cached = this._cache.get(audioBuffer);
1011
1110
  if (cached) {
1012
1111
  if (samplesPerPixel < cached.scale) continue;
1013
1112
  try {
1014
- result.set(clipId, extractPeaks(cached, samplesPerPixel, isMono));
1113
+ const offsets = clipOffsets?.get(clipId);
1114
+ result.set(
1115
+ clipId,
1116
+ extractPeaks(
1117
+ cached,
1118
+ samplesPerPixel,
1119
+ isMono,
1120
+ offsets?.offsetSamples,
1121
+ offsets?.durationSamples
1122
+ )
1123
+ );
1015
1124
  } catch (err) {
1016
1125
  console.warn("[dawcore] reextractPeaks failed for clip " + clipId + ": " + String(err));
1017
1126
  }
@@ -1023,9 +1132,9 @@ var PeakPipeline = class {
1023
1132
  this._worker?.terminate();
1024
1133
  this._worker = null;
1025
1134
  }
1026
- async _getWaveformData(audioBuffer, samplesPerPixel) {
1135
+ async _getWaveformData(audioBuffer) {
1027
1136
  const cached = this._cache.get(audioBuffer);
1028
- if (cached && cached.scale <= samplesPerPixel) return cached;
1137
+ if (cached) return cached;
1029
1138
  const inflight = this._inflight.get(audioBuffer);
1030
1139
  if (inflight) return inflight;
1031
1140
  if (!this._worker) {
@@ -1039,8 +1148,8 @@ var PeakPipeline = class {
1039
1148
  channels,
1040
1149
  length: audioBuffer.length,
1041
1150
  sampleRate: audioBuffer.sampleRate,
1042
- scale: samplesPerPixel,
1043
- bits: 16,
1151
+ scale: this._baseScale,
1152
+ bits: this._bits,
1044
1153
  splitChannels: true
1045
1154
  }).then((waveformData) => {
1046
1155
  this._cache.set(audioBuffer, waveformData);
@@ -1057,7 +1166,7 @@ var PeakPipeline = class {
1057
1166
  };
1058
1167
 
1059
1168
  // src/elements/daw-track-controls.ts
1060
- import { LitElement as LitElement7, html as html6, css as css4 } from "lit";
1169
+ import { LitElement as LitElement7, html as html6, css as css5 } from "lit";
1061
1170
  import { customElement as customElement9, property as property5 } from "lit/decorators.js";
1062
1171
  var DawTrackControlsElement = class extends LitElement7 {
1063
1172
  constructor() {
@@ -1157,11 +1266,11 @@ var DawTrackControlsElement = class extends LitElement7 {
1157
1266
  `;
1158
1267
  }
1159
1268
  };
1160
- DawTrackControlsElement.styles = css4`
1269
+ DawTrackControlsElement.styles = css5`
1161
1270
  :host {
1162
1271
  display: flex;
1163
1272
  flex-direction: column;
1164
- justify-content: center;
1273
+ justify-content: flex-start;
1165
1274
  box-sizing: border-box;
1166
1275
  padding: 6px 8px;
1167
1276
  background: var(--daw-controls-background, #0f0f1a);
@@ -1169,13 +1278,14 @@ DawTrackControlsElement.styles = css4`
1169
1278
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1170
1279
  font-family: system-ui, sans-serif;
1171
1280
  font-size: 11px;
1281
+ overflow: hidden;
1172
1282
  }
1173
1283
  .header {
1174
1284
  display: flex;
1175
1285
  align-items: center;
1176
1286
  justify-content: space-between;
1177
1287
  gap: 4px;
1178
- margin-bottom: 6px;
1288
+ margin-bottom: 3px;
1179
1289
  }
1180
1290
  .name {
1181
1291
  flex: 1;
@@ -1202,7 +1312,7 @@ DawTrackControlsElement.styles = css4`
1202
1312
  .buttons {
1203
1313
  display: flex;
1204
1314
  gap: 3px;
1205
- margin-bottom: 6px;
1315
+ margin-bottom: 3px;
1206
1316
  }
1207
1317
  .btn {
1208
1318
  background: rgba(255, 255, 255, 0.06);
@@ -1232,7 +1342,7 @@ DawTrackControlsElement.styles = css4`
1232
1342
  display: flex;
1233
1343
  align-items: center;
1234
1344
  gap: 4px;
1235
- height: 20px;
1345
+ height: 16px;
1236
1346
  }
1237
1347
  .slider-label {
1238
1348
  width: 50px;
@@ -1312,8 +1422,8 @@ DawTrackControlsElement = __decorateClass([
1312
1422
  ], DawTrackControlsElement);
1313
1423
 
1314
1424
  // src/styles/theme.ts
1315
- import { css as css5 } from "lit";
1316
- var hostStyles = css5`
1425
+ import { css as css6 } from "lit";
1426
+ var hostStyles = css6`
1317
1427
  :host {
1318
1428
  --daw-wave-color: #c49a6c;
1319
1429
  --daw-progress-color: #63c75f;
@@ -1329,6 +1439,77 @@ var hostStyles = css5`
1329
1439
  --daw-clip-header-text: #e0d4c8;
1330
1440
  }
1331
1441
  `;
1442
+ var clipStyles = css6`
1443
+ .clip-container {
1444
+ position: absolute;
1445
+ overflow: hidden;
1446
+ }
1447
+ .clip-header {
1448
+ position: relative;
1449
+ z-index: 1;
1450
+ height: 20px;
1451
+ background: var(--daw-clip-header-background, rgba(0, 0, 0, 0.4));
1452
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
1453
+ display: flex;
1454
+ align-items: center;
1455
+ padding: 0 6px;
1456
+ user-select: none;
1457
+ -webkit-user-drag: none;
1458
+ }
1459
+ .clip-header span {
1460
+ font-size: 10px;
1461
+ font-weight: 500;
1462
+ letter-spacing: 0.02em;
1463
+ font-family: system-ui, sans-serif;
1464
+ color: var(--daw-clip-header-text, #e0d4c8);
1465
+ white-space: nowrap;
1466
+ overflow: hidden;
1467
+ text-overflow: ellipsis;
1468
+ opacity: 0.8;
1469
+ }
1470
+ .clip-boundary {
1471
+ position: absolute;
1472
+ top: 0;
1473
+ width: 8px;
1474
+ height: 100%;
1475
+ z-index: 2;
1476
+ cursor: col-resize;
1477
+ background: transparent;
1478
+ border: none;
1479
+ touch-action: none;
1480
+ user-select: none;
1481
+ -webkit-user-drag: none;
1482
+ transition: background 0.1s, border-color 0.1s;
1483
+ }
1484
+ .clip-boundary[data-boundary-edge='left'] {
1485
+ left: 0;
1486
+ }
1487
+ .clip-boundary[data-boundary-edge='right'] {
1488
+ right: 0;
1489
+ }
1490
+ .clip-boundary[data-boundary-edge='left']:hover {
1491
+ background: rgba(255, 255, 255, 0.2);
1492
+ border-left: 2px solid rgba(255, 255, 255, 0.5);
1493
+ }
1494
+ .clip-boundary[data-boundary-edge='right']:hover {
1495
+ background: rgba(255, 255, 255, 0.2);
1496
+ border-right: 2px solid rgba(255, 255, 255, 0.5);
1497
+ }
1498
+ .clip-boundary[data-boundary-edge='left'].dragging {
1499
+ background: rgba(255, 255, 255, 0.4);
1500
+ border-left: 2px solid rgba(255, 255, 255, 0.8);
1501
+ }
1502
+ .clip-boundary[data-boundary-edge='right'].dragging {
1503
+ background: rgba(255, 255, 255, 0.4);
1504
+ border-right: 2px solid rgba(255, 255, 255, 0.8);
1505
+ }
1506
+ .clip-header[data-interactive] {
1507
+ cursor: grab;
1508
+ }
1509
+ .clip-header[data-interactive]:active {
1510
+ cursor: grabbing;
1511
+ }
1512
+ `;
1332
1513
 
1333
1514
  // src/controllers/viewport-controller.ts
1334
1515
  var OVERSCAN_MULTIPLIER = 1.5;
@@ -1511,6 +1692,9 @@ var RecordingController = class {
1511
1692
  }
1512
1693
  const channelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount ?? 1;
1513
1694
  const startSample = options.startSample ?? Math.floor(this._host._currentTime * this._host.effectiveSampleRate);
1695
+ const outputLatency = rawCtx.outputLatency ?? 0;
1696
+ const lookAhead = context.lookAhead ?? 0;
1697
+ const latencySamples = Math.floor((outputLatency + lookAhead) * rawCtx.sampleRate);
1514
1698
  const source = context.createMediaStreamSource(stream);
1515
1699
  const workletNode = context.createAudioWorkletNode("recording-processor", {
1516
1700
  channelCount,
@@ -1537,6 +1721,8 @@ var RecordingController = class {
1537
1721
  channelCount,
1538
1722
  bits,
1539
1723
  isFirstMessage: true,
1724
+ latencySamples,
1725
+ wasOverdub: options.overdub ?? false,
1540
1726
  _onTrackEnded: onTrackEnded,
1541
1727
  _audioTrack: audioTrack
1542
1728
  };
@@ -1557,6 +1743,9 @@ var RecordingController = class {
1557
1743
  })
1558
1744
  );
1559
1745
  this._host.requestUpdate();
1746
+ if (options.overdub && typeof this._host.play === "function") {
1747
+ await this._host.play(this._host._currentTime);
1748
+ }
1560
1749
  } catch (err) {
1561
1750
  this._cleanupSession(trackId);
1562
1751
  console.warn("[dawcore] RecordingController: Failed to start recording: " + String(err));
@@ -1569,11 +1758,28 @@ var RecordingController = class {
1569
1758
  );
1570
1759
  }
1571
1760
  }
1761
+ pauseRecording(trackId) {
1762
+ const id = trackId ?? [...this._sessions.keys()][0];
1763
+ if (!id) return;
1764
+ const session = this._sessions.get(id);
1765
+ if (!session) return;
1766
+ session.workletNode.port.postMessage({ command: "pause" });
1767
+ }
1768
+ resumeRecording(trackId) {
1769
+ const id = trackId ?? [...this._sessions.keys()][0];
1770
+ if (!id) return;
1771
+ const session = this._sessions.get(id);
1772
+ if (!session) return;
1773
+ session.workletNode.port.postMessage({ command: "resume" });
1774
+ }
1572
1775
  stopRecording(trackId) {
1573
1776
  const id = trackId ?? [...this._sessions.keys()][0];
1574
1777
  if (!id) return;
1575
1778
  const session = this._sessions.get(id);
1576
1779
  if (!session) return;
1780
+ if (session.wasOverdub && typeof this._host.stop === "function") {
1781
+ this._host.stop();
1782
+ }
1577
1783
  session.workletNode.port.postMessage({ command: "stop" });
1578
1784
  session.source.disconnect();
1579
1785
  session.workletNode.disconnect();
@@ -1591,7 +1797,8 @@ var RecordingController = class {
1591
1797
  );
1592
1798
  return;
1593
1799
  }
1594
- const stopCtx = getGlobalContext().rawContext;
1800
+ const context = getGlobalContext();
1801
+ const stopCtx = context.rawContext;
1595
1802
  const channelData = session.chunks.map((chunkArr) => concatenateAudioData(chunkArr));
1596
1803
  const audioBuffer = createAudioBuffer(
1597
1804
  stopCtx,
@@ -1599,7 +1806,21 @@ var RecordingController = class {
1599
1806
  this._host.effectiveSampleRate,
1600
1807
  session.channelCount
1601
1808
  );
1602
- const durationSamples = audioBuffer.length;
1809
+ const latencyOffsetSamples = session.latencySamples;
1810
+ const effectiveDuration = Math.max(0, audioBuffer.length - latencyOffsetSamples);
1811
+ if (effectiveDuration === 0) {
1812
+ console.warn("[dawcore] RecordingController: Recording too short for latency compensation");
1813
+ this._sessions.delete(id);
1814
+ this._host.requestUpdate();
1815
+ this._host.dispatchEvent(
1816
+ new CustomEvent("daw-recording-error", {
1817
+ bubbles: true,
1818
+ composed: true,
1819
+ detail: { trackId: id, error: new Error("Recording too short to save") }
1820
+ })
1821
+ );
1822
+ return;
1823
+ }
1603
1824
  const event = new CustomEvent("daw-recording-complete", {
1604
1825
  bubbles: true,
1605
1826
  composed: true,
@@ -1608,14 +1829,21 @@ var RecordingController = class {
1608
1829
  trackId: id,
1609
1830
  audioBuffer,
1610
1831
  startSample: session.startSample,
1611
- durationSamples
1832
+ durationSamples: effectiveDuration,
1833
+ offsetSamples: latencyOffsetSamples
1612
1834
  }
1613
1835
  });
1614
1836
  const notPrevented = this._host.dispatchEvent(event);
1615
1837
  this._sessions.delete(id);
1616
1838
  this._host.requestUpdate();
1617
1839
  if (notPrevented) {
1618
- this._createClipFromRecording(id, audioBuffer, session.startSample, durationSamples);
1840
+ this._createClipFromRecording(
1841
+ id,
1842
+ audioBuffer,
1843
+ session.startSample,
1844
+ effectiveDuration,
1845
+ latencyOffsetSamples
1846
+ );
1619
1847
  }
1620
1848
  }
1621
1849
  // Session fields are mutated in place on the hot path (~60fps worklet messages).
@@ -1646,7 +1874,9 @@ var RecordingController = class {
1646
1874
  );
1647
1875
  const newPeakCount = Math.floor(session.peaks[ch].length / 2);
1648
1876
  const waveformSelector = `daw-waveform[data-recording-track="${trackId}"][data-recording-channel="${ch}"]`;
1649
- const waveformEl = this._host.shadowRoot?.querySelector(waveformSelector);
1877
+ const waveformEl = this._host.shadowRoot?.querySelector(
1878
+ waveformSelector
1879
+ );
1650
1880
  if (waveformEl) {
1651
1881
  if (session.isFirstMessage) {
1652
1882
  waveformEl.peaks = session.peaks[ch];
@@ -1665,9 +1895,15 @@ var RecordingController = class {
1665
1895
  this._host.requestUpdate();
1666
1896
  }
1667
1897
  }
1668
- _createClipFromRecording(trackId, audioBuffer, startSample, durationSamples) {
1898
+ _createClipFromRecording(trackId, audioBuffer, startSample, durationSamples, offsetSamples = 0) {
1669
1899
  if (typeof this._host._addRecordedClip === "function") {
1670
- this._host._addRecordedClip(trackId, audioBuffer, startSample, durationSamples);
1900
+ this._host._addRecordedClip(
1901
+ trackId,
1902
+ audioBuffer,
1903
+ startSample,
1904
+ durationSamples,
1905
+ offsetSamples
1906
+ );
1671
1907
  } else {
1672
1908
  console.warn(
1673
1909
  '[dawcore] RecordingController: host does not implement _addRecordedClip \u2014 clip not created for track "' + trackId + '"'
@@ -1698,6 +1934,11 @@ var RecordingController = class {
1698
1934
 
1699
1935
  // src/interactions/pointer-handler.ts
1700
1936
  import { pixelsToSeconds } from "@waveform-playlist/core";
1937
+
1938
+ // src/interactions/constants.ts
1939
+ var DRAG_THRESHOLD = 3;
1940
+
1941
+ // src/interactions/pointer-handler.ts
1701
1942
  var PointerHandler = class {
1702
1943
  constructor(host) {
1703
1944
  this._isDragging = false;
@@ -1706,6 +1947,34 @@ var PointerHandler = class {
1706
1947
  // Cached from onPointerDown to avoid forced layout reflows at 60fps during drag
1707
1948
  this._timelineRect = null;
1708
1949
  this.onPointerDown = (e) => {
1950
+ const clipHandler = this._host._clipHandler;
1951
+ if (clipHandler) {
1952
+ const target = e.composedPath()[0];
1953
+ if (target && clipHandler.tryHandle(target, e)) {
1954
+ e.preventDefault();
1955
+ this._timeline = this._host.shadowRoot?.querySelector(".timeline");
1956
+ if (this._timeline) {
1957
+ this._timeline.setPointerCapture(e.pointerId);
1958
+ const onMove = (me) => clipHandler.onPointerMove(me);
1959
+ const onUp = (ue) => {
1960
+ clipHandler.onPointerUp(ue);
1961
+ this._timeline?.removeEventListener("pointermove", onMove);
1962
+ this._timeline?.removeEventListener("pointerup", onUp);
1963
+ try {
1964
+ this._timeline?.releasePointerCapture(ue.pointerId);
1965
+ } catch (err) {
1966
+ console.warn(
1967
+ "[dawcore] releasePointerCapture failed (may already be released): " + String(err)
1968
+ );
1969
+ }
1970
+ this._timeline = null;
1971
+ };
1972
+ this._timeline.addEventListener("pointermove", onMove);
1973
+ this._timeline.addEventListener("pointerup", onUp);
1974
+ }
1975
+ return;
1976
+ }
1977
+ }
1709
1978
  this._timeline = this._host.shadowRoot?.querySelector(".timeline");
1710
1979
  if (!this._timeline) return;
1711
1980
  this._timelineRect = this._timeline.getBoundingClientRect();
@@ -1718,7 +1987,7 @@ var PointerHandler = class {
1718
1987
  this._onPointerMove = (e) => {
1719
1988
  if (!this._timeline) return;
1720
1989
  const currentPx = this._pxFromPointer(e);
1721
- if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > 3) {
1990
+ if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > DRAG_THRESHOLD) {
1722
1991
  this._isDragging = true;
1723
1992
  }
1724
1993
  if (this._isDragging) {
@@ -1853,6 +2122,261 @@ var PointerHandler = class {
1853
2122
  }
1854
2123
  };
1855
2124
 
2125
+ // src/interactions/clip-pointer-handler.ts
2126
+ var ClipPointerHandler = class {
2127
+ constructor(host) {
2128
+ this._mode = null;
2129
+ this._clipId = "";
2130
+ this._trackId = "";
2131
+ this._startPx = 0;
2132
+ this._isDragging = false;
2133
+ this._lastDeltaPx = 0;
2134
+ this._cumulativeDeltaSamples = 0;
2135
+ // Trim visual feedback: snapshot of original clip state
2136
+ this._clipContainer = null;
2137
+ this._boundaryEl = null;
2138
+ this._originalLeft = 0;
2139
+ this._originalWidth = 0;
2140
+ this._originalOffsetSamples = 0;
2141
+ this._originalDurationSamples = 0;
2142
+ this._host = host;
2143
+ }
2144
+ /** Returns true if a drag interaction is currently in progress. */
2145
+ get isActive() {
2146
+ return this._mode !== null;
2147
+ }
2148
+ /**
2149
+ * Attempts to handle a pointerdown event on the given target element.
2150
+ * Returns true if the target is a recognized clip interaction element.
2151
+ */
2152
+ tryHandle(target, e) {
2153
+ if (!this._host.interactiveClips) return false;
2154
+ const boundary = target.closest?.(".clip-boundary");
2155
+ const header = target.closest?.(".clip-header");
2156
+ if (boundary && boundary.dataset.boundaryEdge !== void 0) {
2157
+ const clipId = boundary.dataset.clipId;
2158
+ const trackId = boundary.dataset.trackId;
2159
+ const edge = boundary.dataset.boundaryEdge;
2160
+ if (!clipId || !trackId || edge !== "left" && edge !== "right") return false;
2161
+ this._beginDrag(edge === "left" ? "trim-left" : "trim-right", clipId, trackId, e);
2162
+ this._boundaryEl = boundary;
2163
+ return true;
2164
+ }
2165
+ if (header && header.dataset.interactive !== void 0) {
2166
+ const clipId = header.dataset.clipId;
2167
+ const trackId = header.dataset.trackId;
2168
+ if (!clipId || !trackId) return false;
2169
+ this._beginDrag("move", clipId, trackId, e);
2170
+ return true;
2171
+ }
2172
+ return false;
2173
+ }
2174
+ _beginDrag(mode, clipId, trackId, e) {
2175
+ this._mode = mode;
2176
+ this._clipId = clipId;
2177
+ this._trackId = trackId;
2178
+ this._startPx = e.clientX;
2179
+ this._isDragging = false;
2180
+ this._lastDeltaPx = 0;
2181
+ this._cumulativeDeltaSamples = 0;
2182
+ if (this._host.engine) {
2183
+ this._host.engine.beginTransaction();
2184
+ } else {
2185
+ console.warn(
2186
+ "[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
2187
+ );
2188
+ }
2189
+ if (mode === "trim-left" || mode === "trim-right") {
2190
+ const container = this._host.shadowRoot?.querySelector(
2191
+ `.clip-container[data-clip-id="${clipId}"]`
2192
+ );
2193
+ if (container) {
2194
+ this._clipContainer = container;
2195
+ this._originalLeft = parseFloat(container.style.left) || 0;
2196
+ this._originalWidth = parseFloat(container.style.width) || 0;
2197
+ } else {
2198
+ console.warn("[dawcore] clip container not found for trim visual feedback: " + clipId);
2199
+ }
2200
+ const engine = this._host.engine;
2201
+ if (engine) {
2202
+ const bounds = engine.getClipBounds(trackId, clipId);
2203
+ if (bounds) {
2204
+ this._originalOffsetSamples = bounds.offsetSamples;
2205
+ this._originalDurationSamples = bounds.durationSamples;
2206
+ }
2207
+ }
2208
+ }
2209
+ }
2210
+ /** Processes pointermove events during an active drag. */
2211
+ onPointerMove(e) {
2212
+ if (this._mode === null) return;
2213
+ const totalDeltaPx = e.clientX - this._startPx;
2214
+ if (!this._isDragging && Math.abs(totalDeltaPx) > DRAG_THRESHOLD) {
2215
+ this._isDragging = true;
2216
+ if (this._boundaryEl) {
2217
+ this._boundaryEl.classList.add("dragging");
2218
+ }
2219
+ }
2220
+ if (!this._isDragging) return;
2221
+ const engine = this._host.engine;
2222
+ if (!engine) return;
2223
+ if (this._mode === "move") {
2224
+ const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
2225
+ this._lastDeltaPx = totalDeltaPx;
2226
+ const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
2227
+ const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2228
+ this._cumulativeDeltaSamples += applied;
2229
+ } else {
2230
+ const boundary = this._mode === "trim-left" ? "left" : "right";
2231
+ const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
2232
+ const deltaSamples = engine.constrainTrimDelta(
2233
+ this._trackId,
2234
+ this._clipId,
2235
+ boundary,
2236
+ rawDeltaSamples
2237
+ );
2238
+ const deltaPx = Math.round(deltaSamples / this._host.samplesPerPixel);
2239
+ this._cumulativeDeltaSamples = deltaSamples;
2240
+ if (this._clipContainer) {
2241
+ if (this._mode === "trim-left") {
2242
+ const newLeft = this._originalLeft + deltaPx;
2243
+ const newWidth = this._originalWidth - deltaPx;
2244
+ if (newWidth > 0) {
2245
+ this._clipContainer.style.left = newLeft + "px";
2246
+ this._clipContainer.style.width = newWidth + "px";
2247
+ const newOffset = this._originalOffsetSamples + deltaSamples;
2248
+ const newDuration = this._originalDurationSamples - deltaSamples;
2249
+ if (this._updateWaveformPeaks(newOffset, newDuration)) {
2250
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2251
+ for (const wf of waveforms) {
2252
+ wf.style.left = "0px";
2253
+ }
2254
+ } else {
2255
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2256
+ for (const wf of waveforms) {
2257
+ wf.style.left = -deltaPx + "px";
2258
+ }
2259
+ }
2260
+ }
2261
+ } else {
2262
+ const newWidth = this._originalWidth + deltaPx;
2263
+ if (newWidth > 0) {
2264
+ this._clipContainer.style.width = newWidth + "px";
2265
+ const newDuration = this._originalDurationSamples + deltaSamples;
2266
+ this._updateWaveformPeaks(this._originalOffsetSamples, newDuration);
2267
+ }
2268
+ }
2269
+ }
2270
+ }
2271
+ }
2272
+ /** Processes pointerup events to finalize and dispatch result events. */
2273
+ onPointerUp(_e) {
2274
+ if (this._mode === null) return;
2275
+ try {
2276
+ if (!this._isDragging || this._cumulativeDeltaSamples === 0) {
2277
+ this._restoreTrimVisual();
2278
+ return;
2279
+ }
2280
+ const engine = this._host.engine;
2281
+ if (this._mode === "move") {
2282
+ if (engine) {
2283
+ engine.updateTrack(this._trackId);
2284
+ this._host.dispatchEvent(
2285
+ new CustomEvent("daw-clip-move", {
2286
+ bubbles: true,
2287
+ composed: true,
2288
+ detail: {
2289
+ trackId: this._trackId,
2290
+ clipId: this._clipId,
2291
+ deltaSamples: this._cumulativeDeltaSamples
2292
+ }
2293
+ })
2294
+ );
2295
+ } else {
2296
+ console.warn(
2297
+ "[dawcore] engine unavailable at move drop \u2014 audio may be out of sync for track " + this._trackId
2298
+ );
2299
+ }
2300
+ } else {
2301
+ this._restoreTrimVisual();
2302
+ const boundary = this._mode === "trim-left" ? "left" : "right";
2303
+ if (engine) {
2304
+ engine.trimClip(this._trackId, this._clipId, boundary, this._cumulativeDeltaSamples);
2305
+ this._host.dispatchEvent(
2306
+ new CustomEvent("daw-clip-trim", {
2307
+ bubbles: true,
2308
+ composed: true,
2309
+ detail: {
2310
+ trackId: this._trackId,
2311
+ clipId: this._clipId,
2312
+ boundary,
2313
+ deltaSamples: this._cumulativeDeltaSamples
2314
+ }
2315
+ })
2316
+ );
2317
+ } else {
2318
+ console.warn(
2319
+ "[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
2320
+ );
2321
+ }
2322
+ }
2323
+ } finally {
2324
+ if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
2325
+ this._host.engine?.commitTransaction();
2326
+ } else {
2327
+ this._host.engine?.abortTransaction();
2328
+ }
2329
+ this._reset();
2330
+ }
2331
+ }
2332
+ /** Re-extract peaks from cache and set on waveform elements during trim drag.
2333
+ * Returns true if peaks were successfully updated. */
2334
+ _updateWaveformPeaks(offsetSamples, durationSamples) {
2335
+ if (!this._clipContainer || durationSamples <= 0) return false;
2336
+ const peakSlice = this._host.reextractClipPeaks(this._clipId, offsetSamples, durationSamples);
2337
+ if (!peakSlice) return false;
2338
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2339
+ for (let i = 0; i < waveforms.length; i++) {
2340
+ const wf = waveforms[i];
2341
+ const channelPeaks = peakSlice.data[i];
2342
+ if (channelPeaks) {
2343
+ wf.peaks = channelPeaks;
2344
+ wf.length = peakSlice.length;
2345
+ }
2346
+ }
2347
+ return true;
2348
+ }
2349
+ /** Restore clip container CSS to original values after trim visual preview. */
2350
+ _restoreTrimVisual() {
2351
+ if (this._clipContainer) {
2352
+ this._clipContainer.style.left = this._originalLeft + "px";
2353
+ this._clipContainer.style.width = this._originalWidth + "px";
2354
+ const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
2355
+ for (const wf of waveforms) {
2356
+ wf.style.left = "0px";
2357
+ }
2358
+ }
2359
+ }
2360
+ _reset() {
2361
+ if (this._boundaryEl) {
2362
+ this._boundaryEl.classList.remove("dragging");
2363
+ this._boundaryEl = null;
2364
+ }
2365
+ this._mode = null;
2366
+ this._clipId = "";
2367
+ this._trackId = "";
2368
+ this._startPx = 0;
2369
+ this._isDragging = false;
2370
+ this._lastDeltaPx = 0;
2371
+ this._cumulativeDeltaSamples = 0;
2372
+ this._clipContainer = null;
2373
+ this._originalLeft = 0;
2374
+ this._originalWidth = 0;
2375
+ this._originalOffsetSamples = 0;
2376
+ this._originalDurationSamples = 0;
2377
+ }
2378
+ };
2379
+
1856
2380
  // src/interactions/file-loader.ts
1857
2381
  import { createClipFromSeconds, createTrack } from "@waveform-playlist/core";
1858
2382
  async function loadFiles(host, files) {
@@ -1887,10 +2411,16 @@ async function loadFiles(host, files) {
1887
2411
  sourceDuration: audioBuffer.duration
1888
2412
  });
1889
2413
  host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
2414
+ host._clipOffsets.set(clip.id, {
2415
+ offsetSamples: clip.offsetSamples,
2416
+ durationSamples: clip.durationSamples
2417
+ });
1890
2418
  const peakData = await host._peakPipeline.generatePeaks(
1891
2419
  audioBuffer,
1892
2420
  host.samplesPerPixel,
1893
- host.mono
2421
+ host.mono,
2422
+ clip.offsetSamples,
2423
+ clip.durationSamples
1894
2424
  );
1895
2425
  host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
1896
2426
  const trackId = crypto.randomUUID();
@@ -1949,17 +2479,31 @@ async function loadFiles(host, files) {
1949
2479
 
1950
2480
  // src/interactions/recording-clip.ts
1951
2481
  import { createClip } from "@waveform-playlist/core";
1952
- function addRecordedClip(host, trackId, buf, startSample, durSamples) {
2482
+ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
2483
+ let trimmedBuf = buf;
2484
+ if (offsetSamples > 0 && offsetSamples < buf.length) {
2485
+ const trimmed = new AudioBuffer({
2486
+ numberOfChannels: buf.numberOfChannels,
2487
+ length: durSamples,
2488
+ sampleRate: buf.sampleRate
2489
+ });
2490
+ for (let ch = 0; ch < buf.numberOfChannels; ch++) {
2491
+ const source = buf.getChannelData(ch);
2492
+ trimmed.copyToChannel(source.subarray(offsetSamples, offsetSamples + durSamples), ch);
2493
+ }
2494
+ trimmedBuf = trimmed;
2495
+ }
1953
2496
  const clip = createClip({
1954
- audioBuffer: buf,
2497
+ audioBuffer: trimmedBuf,
1955
2498
  startSample,
1956
2499
  durationSamples: durSamples,
1957
2500
  offsetSamples: 0,
2501
+ // offset already applied by slicing
1958
2502
  gain: 1,
1959
2503
  name: "Recording"
1960
2504
  });
1961
- host._clipBuffers = new Map(host._clipBuffers).set(clip.id, buf);
1962
- host._peakPipeline.generatePeaks(buf, host.samplesPerPixel, host.mono).then((pd) => {
2505
+ host._clipBuffers = new Map(host._clipBuffers).set(clip.id, trimmedBuf);
2506
+ host._peakPipeline.generatePeaks(trimmedBuf, host.samplesPerPixel, host.mono).then((pd) => {
1963
2507
  host._peaksData = new Map(host._peaksData).set(clip.id, pd);
1964
2508
  const t = host._engineTracks.get(trackId);
1965
2509
  if (!t) {
@@ -1992,7 +2536,12 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
1992
2536
  });
1993
2537
  }
1994
2538
  host._recomputeDuration();
1995
- host._engine?.setTracks([...host._engineTracks.values()]);
2539
+ const updatedTrack = host._engineTracks.get(trackId);
2540
+ if (host._engine?.updateTrack && updatedTrack) {
2541
+ host._engine.updateTrack(trackId, updatedTrack);
2542
+ } else {
2543
+ host._engine?.setTracks([...host._engineTracks.values()]);
2544
+ }
1996
2545
  }).catch((err) => {
1997
2546
  console.warn("[dawcore] Failed to generate peaks for recorded clip: " + String(err));
1998
2547
  const next = new Map(host._clipBuffers);
@@ -2010,6 +2559,169 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
2010
2559
  });
2011
2560
  }
2012
2561
 
2562
+ // src/interactions/split-handler.ts
2563
+ function splitAtPlayhead(host) {
2564
+ const wasPlaying = host.isPlaying;
2565
+ const time = host.currentTime;
2566
+ if (!canSplitAtTime(host, time)) return false;
2567
+ if (wasPlaying) {
2568
+ host.stop();
2569
+ }
2570
+ let result;
2571
+ try {
2572
+ result = performSplit(host, time);
2573
+ } catch (err) {
2574
+ console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
2575
+ result = false;
2576
+ }
2577
+ if (wasPlaying) {
2578
+ host.play(time);
2579
+ }
2580
+ return result;
2581
+ }
2582
+ function canSplitAtTime(host, time) {
2583
+ const { engine } = host;
2584
+ if (!engine) return false;
2585
+ const state5 = engine.getState();
2586
+ if (!state5.selectedTrackId) return false;
2587
+ const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
2588
+ if (!track) return false;
2589
+ const atSample = Math.round(time * host.effectiveSampleRate);
2590
+ return !!findClipAtSample(track.clips, atSample);
2591
+ }
2592
+ function performSplit(host, time) {
2593
+ const { engine } = host;
2594
+ if (!engine) return false;
2595
+ const stateBefore = engine.getState();
2596
+ const { selectedTrackId, tracks } = stateBefore;
2597
+ if (!selectedTrackId) return false;
2598
+ const track = tracks.find((t) => t.id === selectedTrackId);
2599
+ if (!track) return false;
2600
+ const atSample = Math.round(time * host.effectiveSampleRate);
2601
+ const clip = findClipAtSample(track.clips, atSample);
2602
+ if (!clip) return false;
2603
+ const originalClipId = clip.id;
2604
+ const clipIdsBefore = new Set(track.clips.map((c) => c.id));
2605
+ engine.splitClip(selectedTrackId, originalClipId, atSample);
2606
+ const stateAfter = engine.getState();
2607
+ const trackAfter = stateAfter.tracks.find((t) => t.id === selectedTrackId);
2608
+ if (!trackAfter) {
2609
+ console.warn(
2610
+ '[dawcore] splitAtPlayhead: track "' + selectedTrackId + '" disappeared after split'
2611
+ );
2612
+ return false;
2613
+ }
2614
+ const newClips = trackAfter.clips.filter((c) => !clipIdsBefore.has(c.id));
2615
+ if (newClips.length !== 2) {
2616
+ if (newClips.length > 0) {
2617
+ console.warn(
2618
+ "[dawcore] splitAtPlayhead: expected 2 new clips after split but got " + newClips.length
2619
+ );
2620
+ }
2621
+ return false;
2622
+ }
2623
+ const sorted = [...newClips].sort((a, b) => a.startSample - b.startSample);
2624
+ const leftClipId = sorted[0].id;
2625
+ const rightClipId = sorted[1].id;
2626
+ host.dispatchEvent(
2627
+ new CustomEvent("daw-clip-split", {
2628
+ bubbles: true,
2629
+ composed: true,
2630
+ detail: {
2631
+ trackId: selectedTrackId,
2632
+ originalClipId,
2633
+ leftClipId,
2634
+ rightClipId
2635
+ }
2636
+ })
2637
+ );
2638
+ return true;
2639
+ }
2640
+ function findClipAtSample(clips, atSample) {
2641
+ return clips.find(
2642
+ (c) => atSample > c.startSample && atSample < c.startSample + c.durationSamples
2643
+ );
2644
+ }
2645
+
2646
+ // src/interactions/clip-peak-sync.ts
2647
+ function syncPeaksForChangedClips(host, tracks) {
2648
+ const currentClipIds = /* @__PURE__ */ new Set();
2649
+ for (const track of tracks) {
2650
+ for (const clip of track.clips) {
2651
+ currentClipIds.add(clip.id);
2652
+ const cached = host._clipOffsets.get(clip.id);
2653
+ const needsPeaks = !host._peaksData.has(clip.id) || !cached || cached.offsetSamples !== clip.offsetSamples || cached.durationSamples !== clip.durationSamples;
2654
+ if (!needsPeaks) continue;
2655
+ const audioBuffer = clip.audioBuffer ?? host._clipBuffers.get(clip.id) ?? findAudioBufferForClip(host, clip, track);
2656
+ if (!audioBuffer) {
2657
+ console.warn(
2658
+ "[dawcore] syncPeaksForChangedClips: no AudioBuffer for clip " + clip.id + " \u2014 waveform will be blank"
2659
+ );
2660
+ continue;
2661
+ }
2662
+ host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
2663
+ host._clipOffsets.set(clip.id, {
2664
+ offsetSamples: clip.offsetSamples,
2665
+ durationSamples: clip.durationSamples
2666
+ });
2667
+ host._peakPipeline.generatePeaks(
2668
+ audioBuffer,
2669
+ host.samplesPerPixel,
2670
+ host.mono,
2671
+ clip.offsetSamples,
2672
+ clip.durationSamples
2673
+ ).then((peakData) => {
2674
+ host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
2675
+ }).catch((err) => {
2676
+ console.warn(
2677
+ "[dawcore] Failed to generate peaks for clip " + clip.id + ": " + String(err)
2678
+ );
2679
+ });
2680
+ }
2681
+ }
2682
+ cleanupOrphanedClipData(host, currentClipIds);
2683
+ }
2684
+ function cleanupOrphanedClipData(host, currentClipIds) {
2685
+ let buffersChanged = false;
2686
+ let peaksChanged = false;
2687
+ for (const id of host._clipBuffers.keys()) {
2688
+ if (!currentClipIds.has(id)) {
2689
+ host._clipBuffers.delete(id);
2690
+ buffersChanged = true;
2691
+ }
2692
+ }
2693
+ let offsetsChanged = false;
2694
+ for (const id of host._clipOffsets.keys()) {
2695
+ if (!currentClipIds.has(id)) {
2696
+ host._clipOffsets.delete(id);
2697
+ offsetsChanged = true;
2698
+ }
2699
+ }
2700
+ for (const id of host._peaksData.keys()) {
2701
+ if (!currentClipIds.has(id)) {
2702
+ host._peaksData.delete(id);
2703
+ peaksChanged = true;
2704
+ }
2705
+ }
2706
+ if (buffersChanged) {
2707
+ host._clipBuffers = new Map(host._clipBuffers);
2708
+ }
2709
+ if (offsetsChanged) {
2710
+ host._clipOffsets = new Map(host._clipOffsets);
2711
+ }
2712
+ if (peaksChanged) {
2713
+ host._peaksData = new Map(host._peaksData);
2714
+ }
2715
+ }
2716
+ function findAudioBufferForClip(host, clip, track) {
2717
+ for (const sibling of track.clips) {
2718
+ if (sibling.id === clip.id) continue;
2719
+ const buf = host._clipBuffers.get(sibling.id);
2720
+ if (buf) return buf;
2721
+ }
2722
+ return null;
2723
+ }
2724
+
2013
2725
  // src/elements/daw-editor.ts
2014
2726
  var DawEditorElement = class extends LitElement8 {
2015
2727
  constructor() {
@@ -2021,6 +2733,9 @@ var DawEditorElement = class extends LitElement8 {
2021
2733
  this.barWidth = 1;
2022
2734
  this.barGap = 0;
2023
2735
  this.fileDrop = false;
2736
+ this.clipHeaders = false;
2737
+ this.clipHeaderHeight = 20;
2738
+ this.interactiveClips = false;
2024
2739
  this.sampleRate = 48e3;
2025
2740
  /** Resolved sample rate — falls back to sampleRate property until first audio decode. */
2026
2741
  this._resolvedSampleRate = null;
@@ -2037,14 +2752,15 @@ var DawEditorElement = class extends LitElement8 {
2037
2752
  this._currentTime = 0;
2038
2753
  this._engine = null;
2039
2754
  this._enginePromise = null;
2040
- this._audioInitialized = false;
2041
2755
  this._audioCache = /* @__PURE__ */ new Map();
2042
2756
  this._clipBuffers = /* @__PURE__ */ new Map();
2757
+ this._clipOffsets = /* @__PURE__ */ new Map();
2043
2758
  this._peakPipeline = new PeakPipeline();
2044
2759
  this._trackElements = /* @__PURE__ */ new Map();
2045
2760
  this._childObserver = null;
2046
2761
  this._audioResume = new AudioResumeController(this);
2047
2762
  this._recordingController = new RecordingController(this);
2763
+ this._clipPointer = new ClipPointerHandler(this);
2048
2764
  this._pointer = new PointerHandler(this);
2049
2765
  this._viewport = (() => {
2050
2766
  const v = new ViewportController(this);
@@ -2088,6 +2804,19 @@ var DawEditorElement = class extends LitElement8 {
2088
2804
  this._onTrackControl = (e) => {
2089
2805
  const { trackId, prop, value } = e.detail ?? {};
2090
2806
  if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
2807
+ if (this._selectedTrackId !== trackId) {
2808
+ this._setSelectedTrackId(trackId);
2809
+ if (this._engine) {
2810
+ this._engine.selectTrack(trackId);
2811
+ }
2812
+ this.dispatchEvent(
2813
+ new CustomEvent("daw-track-select", {
2814
+ bubbles: true,
2815
+ composed: true,
2816
+ detail: { trackId }
2817
+ })
2818
+ );
2819
+ }
2091
2820
  const oldDescriptor = this._tracks.get(trackId);
2092
2821
  if (oldDescriptor) {
2093
2822
  const descriptor = { ...oldDescriptor, [prop]: value };
@@ -2148,6 +2877,28 @@ var DawEditorElement = class extends LitElement8 {
2148
2877
  // --- Recording ---
2149
2878
  this.recordingStream = null;
2150
2879
  }
2880
+ get _clipHandler() {
2881
+ return this.interactiveClips ? this._clipPointer : null;
2882
+ }
2883
+ get engine() {
2884
+ return this._engine;
2885
+ }
2886
+ /** Re-extract peaks for a clip at new offset/duration from cached WaveformData. */
2887
+ reextractClipPeaks(clipId, offsetSamples, durationSamples) {
2888
+ const buf = this._clipBuffers.get(clipId);
2889
+ if (!buf) return null;
2890
+ const singleClipBuffers = /* @__PURE__ */ new Map([[clipId, buf]]);
2891
+ const singleClipOffsets = /* @__PURE__ */ new Map([[clipId, { offsetSamples, durationSamples }]]);
2892
+ const result = this._peakPipeline.reextractPeaks(
2893
+ singleClipBuffers,
2894
+ this.samplesPerPixel,
2895
+ this.mono,
2896
+ singleClipOffsets
2897
+ );
2898
+ const peakData = result.get(clipId);
2899
+ if (!peakData) return null;
2900
+ return { data: peakData.data, length: peakData.length };
2901
+ }
2151
2902
  get effectiveSampleRate() {
2152
2903
  return this._resolvedSampleRate ?? this.sampleRate;
2153
2904
  }
@@ -2222,6 +2973,7 @@ var DawEditorElement = class extends LitElement8 {
2222
2973
  this._trackElements.clear();
2223
2974
  this._audioCache.clear();
2224
2975
  this._clipBuffers.clear();
2976
+ this._clipOffsets.clear();
2225
2977
  this._peakPipeline.terminate();
2226
2978
  try {
2227
2979
  this._disposeEngine();
@@ -2233,17 +2985,19 @@ var DawEditorElement = class extends LitElement8 {
2233
2985
  if (changedProperties.has("eagerResume")) {
2234
2986
  this._audioResume.target = this.eagerResume;
2235
2987
  }
2988
+ if (changedProperties.has("samplesPerPixel") && this._isPlaying) {
2989
+ this._startPlayhead();
2990
+ }
2236
2991
  if (changedProperties.has("samplesPerPixel") && this._clipBuffers.size > 0) {
2237
- const reextracted = this._peakPipeline.reextractPeaks(
2992
+ const re = this._peakPipeline.reextractPeaks(
2238
2993
  this._clipBuffers,
2239
2994
  this.samplesPerPixel,
2240
- this.mono
2995
+ this.mono,
2996
+ this._clipOffsets
2241
2997
  );
2242
- if (reextracted.size > 0) {
2998
+ if (re.size > 0) {
2243
2999
  const next = new Map(this._peaksData);
2244
- for (const [clipId, peakData] of reextracted) {
2245
- next.set(clipId, peakData);
2246
- }
3000
+ for (const [id, pd] of re) next.set(id, pd);
2247
3001
  this._peaksData = next;
2248
3002
  }
2249
3003
  }
@@ -2255,6 +3009,7 @@ var DawEditorElement = class extends LitElement8 {
2255
3009
  const nextPeaks = new Map(this._peaksData);
2256
3010
  for (const clip of removedTrack.clips) {
2257
3011
  this._clipBuffers.delete(clip.id);
3012
+ this._clipOffsets.delete(clip.id);
2258
3013
  nextPeaks.delete(clip.id);
2259
3014
  }
2260
3015
  this._peaksData = nextPeaks;
@@ -2333,10 +3088,16 @@ var DawEditorElement = class extends LitElement8 {
2333
3088
  sourceDuration: audioBuffer.duration
2334
3089
  });
2335
3090
  this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
3091
+ this._clipOffsets.set(clip.id, {
3092
+ offsetSamples: clip.offsetSamples,
3093
+ durationSamples: clip.durationSamples
3094
+ });
2336
3095
  const peakData = await this._peakPipeline.generatePeaks(
2337
3096
  audioBuffer,
2338
3097
  this.samplesPerPixel,
2339
- this.mono
3098
+ this.mono,
3099
+ clip.offsetSamples,
3100
+ clip.durationSamples
2340
3101
  );
2341
3102
  this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
2342
3103
  clips.push(clip);
@@ -2428,10 +3189,20 @@ var DawEditorElement = class extends LitElement8 {
2428
3189
  samplesPerPixel: this.samplesPerPixel,
2429
3190
  zoomLevels: [256, 512, 1024, 2048, 4096, 8192, this.samplesPerPixel].filter((v, i, a) => a.indexOf(v) === i).sort((a, b) => a - b)
2430
3191
  });
3192
+ let lastTracksVersion = -1;
2431
3193
  engine.on("statechange", (engineState) => {
2432
3194
  this._isPlaying = engineState.isPlaying;
2433
3195
  this._duration = engineState.duration;
2434
3196
  this._selectedTrackId = engineState.selectedTrackId;
3197
+ if (engineState.tracksVersion !== lastTracksVersion) {
3198
+ lastTracksVersion = engineState.tracksVersion;
3199
+ const nextTracks = /* @__PURE__ */ new Map();
3200
+ for (const track of engineState.tracks) {
3201
+ nextTracks.set(track.id, track);
3202
+ }
3203
+ this._engineTracks = nextTracks;
3204
+ syncPeaksForChangedClips(this, engineState.tracks);
3205
+ }
2435
3206
  });
2436
3207
  engine.on("timeupdate", (time) => {
2437
3208
  this._currentTime = time;
@@ -2454,14 +3225,11 @@ var DawEditorElement = class extends LitElement8 {
2454
3225
  return loadFiles(this, files);
2455
3226
  }
2456
3227
  // --- Playback ---
2457
- async play() {
3228
+ async play(startTime) {
2458
3229
  try {
2459
3230
  const engine = await this._ensureEngine();
2460
- if (!this._audioInitialized) {
2461
- await engine.init();
2462
- this._audioInitialized = true;
2463
- }
2464
- engine.play();
3231
+ await engine.init();
3232
+ engine.play(startTime);
2465
3233
  this._startPlayhead();
2466
3234
  this.dispatchEvent(new CustomEvent("daw-play", { bubbles: true, composed: true }));
2467
3235
  } catch (err) {
@@ -2487,19 +3255,90 @@ var DawEditorElement = class extends LitElement8 {
2487
3255
  this._stopPlayhead();
2488
3256
  this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
2489
3257
  }
3258
+ /** Toggle between play and pause. */
3259
+ togglePlayPause() {
3260
+ if (this._isPlaying) {
3261
+ this.pause();
3262
+ } else {
3263
+ this.play();
3264
+ }
3265
+ }
2490
3266
  seekTo(time) {
2491
- if (!this._engine) return;
2492
- this._engine.seek(time);
2493
- this._currentTime = time;
3267
+ if (!this._engine) {
3268
+ console.warn("[dawcore] seekTo: engine not ready, call ignored");
3269
+ return;
3270
+ }
3271
+ if (this._isPlaying) {
3272
+ this.stop();
3273
+ this.play(time);
3274
+ } else {
3275
+ this._engine.seek(time);
3276
+ this._currentTime = time;
3277
+ this._stopPlayhead();
3278
+ }
3279
+ }
3280
+ /** Undo the last structural edit. */
3281
+ undo() {
3282
+ if (!this._engine) {
3283
+ console.warn("[dawcore] undo: engine not ready, call ignored");
3284
+ return;
3285
+ }
3286
+ this._engine.undo();
3287
+ }
3288
+ /** Redo the last undone edit. */
3289
+ redo() {
3290
+ if (!this._engine) {
3291
+ console.warn("[dawcore] redo: engine not ready, call ignored");
3292
+ return;
3293
+ }
3294
+ this._engine.redo();
3295
+ }
3296
+ /** Whether undo is available. */
3297
+ get canUndo() {
3298
+ return this._engine?.canUndo ?? false;
3299
+ }
3300
+ /** Whether redo is available. */
3301
+ get canRedo() {
3302
+ return this._engine?.canRedo ?? false;
3303
+ }
3304
+ /** Split the clip under the playhead on the selected track. */
3305
+ splitAtPlayhead() {
3306
+ return splitAtPlayhead({
3307
+ effectiveSampleRate: this.effectiveSampleRate,
3308
+ currentTime: this._currentTime,
3309
+ isPlaying: this._isPlaying,
3310
+ engine: this._engine,
3311
+ dispatchEvent: (e) => this.dispatchEvent(e),
3312
+ stop: () => {
3313
+ this._engine?.stop();
3314
+ this._stopPlayhead();
3315
+ },
3316
+ // Call engine.play directly (synchronous) — not the async editor play()
3317
+ // which yields to microtask queue via await engine.init(). Engine is
3318
+ // already initialized at split time; the async gap causes audio desync.
3319
+ play: (time) => {
3320
+ this._engine?.play(time);
3321
+ this._startPlayhead();
3322
+ }
3323
+ });
3324
+ }
3325
+ get currentTime() {
3326
+ return this._currentTime;
2494
3327
  }
2495
3328
  get isRecording() {
2496
3329
  return this._recordingController.isRecording;
2497
3330
  }
3331
+ pauseRecording() {
3332
+ this._recordingController.pauseRecording();
3333
+ }
3334
+ resumeRecording() {
3335
+ this._recordingController.resumeRecording();
3336
+ }
2498
3337
  stopRecording() {
2499
3338
  this._recordingController.stopRecording();
2500
3339
  }
2501
- _addRecordedClip(trackId, buf, startSample, durSamples) {
2502
- addRecordedClip(this, trackId, buf, startSample, durSamples);
3340
+ _addRecordedClip(trackId, buf, startSample, durSamples, offsetSamples = 0) {
3341
+ addRecordedClip(this, trackId, buf, startSample, durSamples, offsetSamples);
2503
3342
  }
2504
3343
  async startRecording(stream, options) {
2505
3344
  const s = stream ?? this.recordingStream;
@@ -2512,15 +3351,19 @@ var DawEditorElement = class extends LitElement8 {
2512
3351
  _renderRecordingPreview(trackId, chH) {
2513
3352
  const rs = this._recordingController.getSession(trackId);
2514
3353
  if (!rs) return "";
3354
+ const audibleSamples = Math.max(0, rs.totalSamples - rs.latencySamples);
3355
+ if (audibleSamples === 0) return "";
3356
+ const latencyPixels = Math.floor(rs.latencySamples / this.samplesPerPixel);
2515
3357
  const left = Math.floor(rs.startSample / this.samplesPerPixel);
2516
- const w = Math.floor(rs.totalSamples / this.samplesPerPixel);
2517
- return rs.peaks.map(
2518
- (chPeaks, ch) => html7`
3358
+ const w = Math.floor(audibleSamples / this.samplesPerPixel);
3359
+ return rs.peaks.map((chPeaks, ch) => {
3360
+ const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
3361
+ return html7`
2519
3362
  <daw-waveform
2520
3363
  data-recording-track=${trackId}
2521
3364
  data-recording-channel=${ch}
2522
3365
  style="position:absolute;left:${left}px;top:${ch * chH}px;"
2523
- .peaks=${chPeaks}
3366
+ .peaks=${slicedPeaks}
2524
3367
  .length=${w}
2525
3368
  .waveHeight=${chH}
2526
3369
  .barWidth=${this.barWidth}
@@ -2529,8 +3372,8 @@ var DawEditorElement = class extends LitElement8 {
2529
3372
  .visibleEnd=${this._viewport.visibleEnd}
2530
3373
  .originX=${left}
2531
3374
  ></daw-waveform>
2532
- `
2533
- );
3375
+ `;
3376
+ });
2534
3377
  }
2535
3378
  // --- Playhead ---
2536
3379
  _startPlayhead() {
@@ -2572,13 +3415,14 @@ var DawEditorElement = class extends LitElement8 {
2572
3415
  const orderedTracks = this._getOrderedTracks().map(([trackId, track]) => {
2573
3416
  const descriptor = this._tracks.get(trackId);
2574
3417
  const firstPeaks = track.clips.map((c) => this._peaksData.get(c.id)).find((p) => p && p.data.length > 0);
2575
- const numChannels = firstPeaks ? firstPeaks.data.length : 1;
3418
+ const recSession = this._recordingController.getSession(trackId);
3419
+ const numChannels = firstPeaks ? firstPeaks.data.length : recSession ? recSession.channelCount : 1;
2576
3420
  return {
2577
3421
  trackId,
2578
3422
  track,
2579
3423
  descriptor,
2580
3424
  numChannels,
2581
- trackHeight: this.waveHeight * numChannels
3425
+ trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
2582
3426
  };
2583
3427
  });
2584
3428
  return html7`
@@ -2632,21 +3476,47 @@ var DawEditorElement = class extends LitElement8 {
2632
3476
  );
2633
3477
  const clipLeft = Math.floor(clip.startSample / this.samplesPerPixel);
2634
3478
  const channels = peakData?.data ?? [new Int16Array(0)];
2635
- return channels.map(
2636
- (channelPeaks, chIdx) => html7`
2637
- <daw-waveform
2638
- style="position: absolute; left: ${clipLeft}px; top: ${chIdx * channelHeight}px;"
2639
- .peaks=${channelPeaks}
2640
- .length=${peakData?.length ?? width}
2641
- .waveHeight=${channelHeight}
2642
- .barWidth=${this.barWidth}
2643
- .barGap=${this.barGap}
2644
- .visibleStart=${this._viewport.visibleStart}
2645
- .visibleEnd=${this._viewport.visibleEnd}
2646
- .originX=${clipLeft}
2647
- ></daw-waveform>
2648
- `
2649
- );
3479
+ const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
3480
+ const chH = this.waveHeight;
3481
+ return html7` <div
3482
+ class="clip-container"
3483
+ style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
3484
+ data-clip-id=${clip.id}
3485
+ >
3486
+ ${hdrH > 0 ? html7`<div
3487
+ class="clip-header"
3488
+ data-clip-id=${clip.id}
3489
+ data-track-id=${t.trackId}
3490
+ ?data-interactive=${this.interactiveClips}
3491
+ >
3492
+ <span>${clip.name || t.descriptor?.name || ""}</span>
3493
+ </div>` : ""}
3494
+ ${channels.map(
3495
+ (chPeaks, chIdx) => html7` <daw-waveform
3496
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
3497
+ .peaks=${chPeaks}
3498
+ .length=${peakData?.length ?? width}
3499
+ .waveHeight=${chH}
3500
+ .barWidth=${this.barWidth}
3501
+ .barGap=${this.barGap}
3502
+ .visibleStart=${this._viewport.visibleStart}
3503
+ .visibleEnd=${this._viewport.visibleEnd}
3504
+ .originX=${clipLeft}
3505
+ ></daw-waveform>`
3506
+ )}
3507
+ ${this.interactiveClips ? html7` <div
3508
+ class="clip-boundary"
3509
+ data-boundary-edge="left"
3510
+ data-clip-id=${clip.id}
3511
+ data-track-id=${t.trackId}
3512
+ ></div>
3513
+ <div
3514
+ class="clip-boundary"
3515
+ data-boundary-edge="right"
3516
+ data-clip-id=${clip.id}
3517
+ data-track-id=${t.trackId}
3518
+ ></div>` : ""}
3519
+ </div>`;
2650
3520
  })}
2651
3521
  ${this._renderRecordingPreview(t.trackId, channelHeight)}
2652
3522
  </div>
@@ -2660,7 +3530,7 @@ var DawEditorElement = class extends LitElement8 {
2660
3530
  };
2661
3531
  DawEditorElement.styles = [
2662
3532
  hostStyles,
2663
- css6`
3533
+ css7`
2664
3534
  :host {
2665
3535
  display: flex;
2666
3536
  position: relative;
@@ -2694,7 +3564,8 @@ DawEditorElement.styles = [
2694
3564
  outline: 2px dashed var(--daw-selection-color, rgba(99, 199, 95, 0.3));
2695
3565
  outline-offset: -2px;
2696
3566
  }
2697
- `
3567
+ `,
3568
+ clipStyles
2698
3569
  ];
2699
3570
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
2700
3571
  __decorateClass([
@@ -2718,29 +3589,38 @@ __decorateClass([
2718
3589
  __decorateClass([
2719
3590
  property6({ type: Boolean, attribute: "file-drop" })
2720
3591
  ], DawEditorElement.prototype, "fileDrop", 2);
3592
+ __decorateClass([
3593
+ property6({ type: Boolean, attribute: "clip-headers" })
3594
+ ], DawEditorElement.prototype, "clipHeaders", 2);
3595
+ __decorateClass([
3596
+ property6({ type: Number, attribute: "clip-header-height" })
3597
+ ], DawEditorElement.prototype, "clipHeaderHeight", 2);
3598
+ __decorateClass([
3599
+ property6({ type: Boolean, attribute: "interactive-clips" })
3600
+ ], DawEditorElement.prototype, "interactiveClips", 2);
2721
3601
  __decorateClass([
2722
3602
  property6({ type: Number, attribute: "sample-rate" })
2723
3603
  ], DawEditorElement.prototype, "sampleRate", 2);
2724
3604
  __decorateClass([
2725
- state()
3605
+ state3()
2726
3606
  ], DawEditorElement.prototype, "_tracks", 2);
2727
3607
  __decorateClass([
2728
- state()
3608
+ state3()
2729
3609
  ], DawEditorElement.prototype, "_engineTracks", 2);
2730
3610
  __decorateClass([
2731
- state()
3611
+ state3()
2732
3612
  ], DawEditorElement.prototype, "_peaksData", 2);
2733
3613
  __decorateClass([
2734
- state()
3614
+ state3()
2735
3615
  ], DawEditorElement.prototype, "_isPlaying", 2);
2736
3616
  __decorateClass([
2737
- state()
3617
+ state3()
2738
3618
  ], DawEditorElement.prototype, "_duration", 2);
2739
3619
  __decorateClass([
2740
- state()
3620
+ state3()
2741
3621
  ], DawEditorElement.prototype, "_selectedTrackId", 2);
2742
3622
  __decorateClass([
2743
- state()
3623
+ state3()
2744
3624
  ], DawEditorElement.prototype, "_dragOver", 2);
2745
3625
  __decorateClass([
2746
3626
  property6({ attribute: "eager-resume" })
@@ -2750,7 +3630,7 @@ DawEditorElement = __decorateClass([
2750
3630
  ], DawEditorElement);
2751
3631
 
2752
3632
  // src/elements/daw-ruler.ts
2753
- import { LitElement as LitElement9, html as html8, css as css7 } from "lit";
3633
+ import { LitElement as LitElement9, html as html8, css as css8 } from "lit";
2754
3634
  import { customElement as customElement11, property as property7 } from "lit/decorators.js";
2755
3635
 
2756
3636
  // src/utils/time-format.ts
@@ -2883,7 +3763,7 @@ var DawRulerElement = class extends LitElement9 {
2883
3763
  }
2884
3764
  }
2885
3765
  };
2886
- DawRulerElement.styles = css7`
3766
+ DawRulerElement.styles = css8`
2887
3767
  :host {
2888
3768
  display: block;
2889
3769
  position: relative;
@@ -2921,7 +3801,7 @@ DawRulerElement = __decorateClass([
2921
3801
  ], DawRulerElement);
2922
3802
 
2923
3803
  // src/elements/daw-selection.ts
2924
- import { LitElement as LitElement10, html as html9, css as css8 } from "lit";
3804
+ import { LitElement as LitElement10, html as html9, css as css9 } from "lit";
2925
3805
  import { customElement as customElement12, property as property8 } from "lit/decorators.js";
2926
3806
  var DawSelectionElement = class extends LitElement10 {
2927
3807
  constructor() {
@@ -2936,7 +3816,7 @@ var DawSelectionElement = class extends LitElement10 {
2936
3816
  return html9`<div style="left: ${left}px; width: ${width}px;"></div>`;
2937
3817
  }
2938
3818
  };
2939
- DawSelectionElement.styles = css8`
3819
+ DawSelectionElement.styles = css9`
2940
3820
  :host {
2941
3821
  position: absolute;
2942
3822
  top: 0;
@@ -2963,8 +3843,8 @@ DawSelectionElement = __decorateClass([
2963
3843
  ], DawSelectionElement);
2964
3844
 
2965
3845
  // src/elements/daw-record-button.ts
2966
- import { html as html10, css as css9 } from "lit";
2967
- import { customElement as customElement13, state as state2 } from "lit/decorators.js";
3846
+ import { html as html10, css as css10 } from "lit";
3847
+ import { customElement as customElement13, state as state4 } from "lit/decorators.js";
2968
3848
  var DawRecordButtonElement = class extends DawTransportButton {
2969
3849
  constructor() {
2970
3850
  super(...arguments);
@@ -2982,7 +3862,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
2982
3862
  }
2983
3863
  connectedCallback() {
2984
3864
  super.connectedCallback();
2985
- this._listenToTarget();
3865
+ requestAnimationFrame(() => this._listenToTarget());
2986
3866
  }
2987
3867
  disconnectedCallback() {
2988
3868
  super.disconnectedCallback();
@@ -3007,11 +3887,12 @@ var DawRecordButtonElement = class extends DawTransportButton {
3007
3887
  render() {
3008
3888
  return html10`
3009
3889
  <button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
3010
- <slot>${this._isRecording ? "Stop Rec" : "Record"}</slot>
3890
+ <slot>Record</slot>
3011
3891
  </button>
3012
3892
  `;
3013
3893
  }
3014
3894
  _onClick() {
3895
+ if (this._isRecording) return;
3015
3896
  const target = this.target;
3016
3897
  if (!target) {
3017
3898
  console.warn(
@@ -3019,32 +3900,201 @@ var DawRecordButtonElement = class extends DawTransportButton {
3019
3900
  );
3020
3901
  return;
3021
3902
  }
3022
- if (this._isRecording) {
3023
- target.stopRecording();
3024
- } else {
3025
- target.startRecording(target.recordingStream);
3026
- }
3903
+ target.startRecording(target.recordingStream);
3027
3904
  }
3028
3905
  };
3029
3906
  DawRecordButtonElement.styles = [
3030
3907
  DawTransportButton.styles,
3031
- css9`
3908
+ css10`
3032
3909
  button[data-recording] {
3033
3910
  color: #d08070;
3034
3911
  border-color: #d08070;
3912
+ background: rgba(208, 128, 112, 0.15);
3035
3913
  }
3036
3914
  `
3037
3915
  ];
3038
3916
  __decorateClass([
3039
- state2()
3917
+ state4()
3040
3918
  ], DawRecordButtonElement.prototype, "_isRecording", 2);
3041
3919
  DawRecordButtonElement = __decorateClass([
3042
3920
  customElement13("daw-record-button")
3043
3921
  ], DawRecordButtonElement);
3922
+
3923
+ // src/elements/daw-keyboard-shortcuts.ts
3924
+ import { LitElement as LitElement11 } from "lit";
3925
+ import { customElement as customElement14, property as property9 } from "lit/decorators.js";
3926
+ import { handleKeyboardEvent } from "@waveform-playlist/core";
3927
+ var DawKeyboardShortcutsElement = class extends LitElement11 {
3928
+ constructor() {
3929
+ super(...arguments);
3930
+ this.playback = false;
3931
+ this.splitting = false;
3932
+ this.undo = false;
3933
+ // --- JS properties for remapping ---
3934
+ this.playbackShortcuts = null;
3935
+ this.splittingShortcuts = null;
3936
+ this.undoShortcuts = null;
3937
+ /** Additional custom shortcuts. */
3938
+ this.customShortcuts = [];
3939
+ this._editor = null;
3940
+ this._cachedShortcuts = null;
3941
+ // --- Event handler ---
3942
+ this._onKeyDown = (e) => {
3943
+ const shortcuts = this.shortcuts;
3944
+ if (shortcuts.length === 0) return;
3945
+ try {
3946
+ handleKeyboardEvent(e, shortcuts, true);
3947
+ } catch (err) {
3948
+ console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
3949
+ const target = this._editor ?? this;
3950
+ target.dispatchEvent(
3951
+ new CustomEvent("daw-error", {
3952
+ bubbles: true,
3953
+ composed: true,
3954
+ detail: { operation: "keyboard-shortcut", key: e.key, error: err }
3955
+ })
3956
+ );
3957
+ }
3958
+ };
3959
+ }
3960
+ /** All active shortcuts (read-only, cached). */
3961
+ get shortcuts() {
3962
+ if (!this._cachedShortcuts) {
3963
+ this._cachedShortcuts = this._buildShortcuts();
3964
+ }
3965
+ return this._cachedShortcuts;
3966
+ }
3967
+ /** Invalidate cached shortcuts when Lit properties change. */
3968
+ updated() {
3969
+ this._cachedShortcuts = null;
3970
+ }
3971
+ // --- Lifecycle ---
3972
+ connectedCallback() {
3973
+ super.connectedCallback();
3974
+ this._editor = this.closest("daw-editor");
3975
+ if (!this._editor) {
3976
+ console.warn(
3977
+ "[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
3978
+ );
3979
+ }
3980
+ document.addEventListener("keydown", this._onKeyDown);
3981
+ }
3982
+ disconnectedCallback() {
3983
+ super.disconnectedCallback();
3984
+ document.removeEventListener("keydown", this._onKeyDown);
3985
+ this._editor = null;
3986
+ }
3987
+ // No shadow DOM — render-less element
3988
+ createRenderRoot() {
3989
+ return this;
3990
+ }
3991
+ // --- Shortcut building ---
3992
+ _buildShortcuts() {
3993
+ const editor = this._editor;
3994
+ if (!editor) return this.customShortcuts;
3995
+ const result = [];
3996
+ if (this.playback) {
3997
+ const map = this.playbackShortcuts;
3998
+ result.push(
3999
+ this._makeShortcut(
4000
+ map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
4001
+ () => editor.togglePlayPause(),
4002
+ "Play/Pause"
4003
+ ),
4004
+ this._makeShortcut(
4005
+ map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
4006
+ () => editor.stop(),
4007
+ "Stop"
4008
+ ),
4009
+ this._makeShortcut(
4010
+ map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
4011
+ () => editor.seekTo(0),
4012
+ "Rewind to start"
4013
+ )
4014
+ );
4015
+ }
4016
+ if (this.splitting) {
4017
+ const map = this.splittingShortcuts;
4018
+ const binding = map?.splitAtPlayhead ?? {
4019
+ key: "s",
4020
+ ctrlKey: false,
4021
+ metaKey: false,
4022
+ altKey: false
4023
+ };
4024
+ result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
4025
+ }
4026
+ if (this.undo) {
4027
+ const map = this.undoShortcuts;
4028
+ const undoBinding = map?.undo ?? { key: "z" };
4029
+ const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
4030
+ if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
4031
+ const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
4032
+ result.push(
4033
+ this._makeShortcut(
4034
+ { ...undoBinding, ctrlKey: true, ...undoShift },
4035
+ () => editor.undo(),
4036
+ "Undo"
4037
+ ),
4038
+ this._makeShortcut(
4039
+ { ...undoBinding, metaKey: true, ...undoShift },
4040
+ () => editor.undo(),
4041
+ "Undo"
4042
+ )
4043
+ );
4044
+ } else {
4045
+ result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
4046
+ }
4047
+ if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
4048
+ const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
4049
+ result.push(
4050
+ this._makeShortcut(
4051
+ { ...redoBinding, ctrlKey: true, ...redoShift },
4052
+ () => editor.redo(),
4053
+ "Redo"
4054
+ ),
4055
+ this._makeShortcut(
4056
+ { ...redoBinding, metaKey: true, ...redoShift },
4057
+ () => editor.redo(),
4058
+ "Redo"
4059
+ )
4060
+ );
4061
+ } else {
4062
+ result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
4063
+ }
4064
+ }
4065
+ result.push(...this.customShortcuts);
4066
+ return result;
4067
+ }
4068
+ _makeShortcut(binding, action, description) {
4069
+ return {
4070
+ key: binding.key,
4071
+ ...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
4072
+ ...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
4073
+ ...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
4074
+ ...binding.altKey !== void 0 && { altKey: binding.altKey },
4075
+ action,
4076
+ description
4077
+ };
4078
+ }
4079
+ };
4080
+ __decorateClass([
4081
+ property9({ type: Boolean })
4082
+ ], DawKeyboardShortcutsElement.prototype, "playback", 2);
4083
+ __decorateClass([
4084
+ property9({ type: Boolean })
4085
+ ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
4086
+ __decorateClass([
4087
+ property9({ type: Boolean })
4088
+ ], DawKeyboardShortcutsElement.prototype, "undo", 2);
4089
+ DawKeyboardShortcutsElement = __decorateClass([
4090
+ customElement14("daw-keyboard-shortcuts")
4091
+ ], DawKeyboardShortcutsElement);
3044
4092
  export {
3045
4093
  AudioResumeController,
4094
+ ClipPointerHandler,
3046
4095
  DawClipElement,
3047
4096
  DawEditorElement,
4097
+ DawKeyboardShortcutsElement,
3048
4098
  DawPauseButtonElement,
3049
4099
  DawPlayButtonElement,
3050
4100
  DawPlayheadElement,
@@ -3057,6 +4107,7 @@ export {
3057
4107
  DawTransportButton,
3058
4108
  DawTransportElement,
3059
4109
  DawWaveformElement,
3060
- RecordingController
4110
+ RecordingController,
4111
+ splitAtPlayhead
3061
4112
  };
3062
4113
  //# sourceMappingURL=index.mjs.map