@waveform-playlist/ui-components 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -51,8 +51,10 @@ __export(index_exports, {
51
51
  ClipBoundary: () => ClipBoundary,
52
52
  ClipHeader: () => ClipHeader,
53
53
  ClipHeaderPresentational: () => ClipHeaderPresentational,
54
+ CloseButton: () => CloseButton,
54
55
  Controls: () => Controls,
55
56
  DevicePixelRatioProvider: () => DevicePixelRatioProvider,
57
+ DotsIcon: () => DotsIcon,
56
58
  FadeOverlay: () => FadeOverlay,
57
59
  Header: () => Header,
58
60
  InlineLabel: () => InlineLabel,
@@ -71,6 +73,8 @@ __export(index_exports, {
71
73
  SliderWrapper: () => SliderWrapper,
72
74
  SmartChannel: () => SmartChannel,
73
75
  SmartScale: () => SmartScale,
76
+ SpectrogramChannel: () => SpectrogramChannel,
77
+ SpectrogramLabels: () => SpectrogramLabels,
74
78
  StyledPlaylist: () => StyledPlaylist,
75
79
  StyledTimeScale: () => StyledTimeScale,
76
80
  TimeFormatSelect: () => TimeFormatSelect,
@@ -79,7 +83,7 @@ __export(index_exports, {
79
83
  TimescaleLoopRegion: () => TimescaleLoopRegion,
80
84
  Track: () => Track,
81
85
  TrackControlsContext: () => TrackControlsContext,
82
- TrackControlsWithDelete: () => TrackControlsWithDelete,
86
+ TrackMenu: () => TrackMenu,
83
87
  TrashIcon: () => TrashIcon,
84
88
  VolumeDownIcon: () => VolumeDownIcon,
85
89
  VolumeUpIcon: () => VolumeUpIcon,
@@ -1991,16 +1995,285 @@ var PlayoutProvider = ({ children }) => {
1991
1995
  var usePlayoutStatus = () => (0, import_react11.useContext)(PlayoutStatusContext);
1992
1996
  var usePlayoutStatusUpdate = () => (0, import_react11.useContext)(PlayoutStatusUpdateContext);
1993
1997
 
1994
- // src/components/SmartChannel.tsx
1998
+ // src/components/SpectrogramChannel.tsx
1999
+ var import_react12 = require("react");
2000
+ var import_styled_components20 = __toESM(require("styled-components"));
1995
2001
  var import_jsx_runtime18 = require("react/jsx-runtime");
1996
- var SmartChannel = ({ isSelected, transparentBackground, ...props }) => {
2002
+ var MAX_CANVAS_WIDTH2 = 1e3;
2003
+ var Wrapper3 = import_styled_components20.default.div.attrs((props) => ({
2004
+ style: {
2005
+ top: `${props.$waveHeight * props.$index}px`,
2006
+ width: `${props.$cssWidth}px`,
2007
+ height: `${props.$waveHeight}px`
2008
+ }
2009
+ }))`
2010
+ position: absolute;
2011
+ background: #000;
2012
+ transform: translateZ(0);
2013
+ backface-visibility: hidden;
2014
+ `;
2015
+ var SpectrogramCanvas = import_styled_components20.default.canvas.attrs((props) => ({
2016
+ style: {
2017
+ width: `${props.$cssWidth}px`,
2018
+ height: `${props.$waveHeight}px`
2019
+ }
2020
+ }))`
2021
+ float: left;
2022
+ position: relative;
2023
+ will-change: transform;
2024
+ image-rendering: pixelated;
2025
+ image-rendering: crisp-edges;
2026
+ `;
2027
+ function defaultGetColorMap() {
2028
+ const lut = new Uint8Array(256 * 3);
2029
+ for (let i = 0; i < 256; i++) {
2030
+ lut[i * 3] = lut[i * 3 + 1] = lut[i * 3 + 2] = i;
2031
+ }
2032
+ return lut;
2033
+ }
2034
+ var SpectrogramChannel = ({
2035
+ index,
2036
+ data,
2037
+ length,
2038
+ waveHeight,
2039
+ devicePixelRatio = 1,
2040
+ samplesPerPixel,
2041
+ colorLUT,
2042
+ frequencyScaleFn,
2043
+ minFrequency = 0,
2044
+ maxFrequency,
2045
+ workerApi,
2046
+ clipId,
2047
+ onCanvasesReady
2048
+ }) => {
2049
+ const canvasesRef = (0, import_react12.useRef)([]);
2050
+ const registeredIdsRef = (0, import_react12.useRef)([]);
2051
+ const isWorkerMode = !!(workerApi && clipId);
2052
+ const canvasRef = (0, import_react12.useCallback)(
2053
+ (canvas) => {
2054
+ if (canvas !== null) {
2055
+ const idx = parseInt(canvas.dataset.index, 10);
2056
+ canvasesRef.current[idx] = canvas;
2057
+ }
2058
+ },
2059
+ []
2060
+ );
2061
+ (0, import_react12.useEffect)(() => {
2062
+ if (!isWorkerMode) return;
2063
+ const canvasCount2 = Math.ceil(length / MAX_CANVAS_WIDTH2);
2064
+ canvasesRef.current.length = canvasCount2;
2065
+ const canvases2 = canvasesRef.current;
2066
+ const ids = [];
2067
+ const widths = [];
2068
+ for (let i = 0; i < canvases2.length; i++) {
2069
+ const canvas = canvases2[i];
2070
+ if (!canvas) continue;
2071
+ const canvasId = `${clipId}-ch${index}-chunk${i}`;
2072
+ try {
2073
+ const offscreen = canvas.transferControlToOffscreen();
2074
+ workerApi.registerCanvas(canvasId, offscreen);
2075
+ ids.push(canvasId);
2076
+ widths.push(Math.min(length - i * MAX_CANVAS_WIDTH2, MAX_CANVAS_WIDTH2));
2077
+ } catch (err) {
2078
+ console.warn(`[spectrogram] transferControlToOffscreen failed for ${canvasId}:`, err);
2079
+ continue;
2080
+ }
2081
+ }
2082
+ registeredIdsRef.current = ids;
2083
+ if (ids.length > 0 && onCanvasesReady) {
2084
+ onCanvasesReady(ids, widths);
2085
+ }
2086
+ return () => {
2087
+ for (const id of registeredIdsRef.current) {
2088
+ workerApi.unregisterCanvas(id);
2089
+ }
2090
+ registeredIdsRef.current = [];
2091
+ };
2092
+ }, [isWorkerMode, clipId, index, length]);
2093
+ const lut = colorLUT ?? defaultGetColorMap();
2094
+ const maxF = maxFrequency ?? (data ? data.sampleRate / 2 : 22050);
2095
+ const scaleFn = frequencyScaleFn ?? ((f, minF, maxF2) => (f - minF) / (maxF2 - minF));
2096
+ (0, import_react12.useLayoutEffect)(() => {
2097
+ if (isWorkerMode || !data) return;
2098
+ const canvases2 = canvasesRef.current;
2099
+ const { frequencyBinCount, frameCount, hopSize, sampleRate, gainDb, rangeDb: rawRangeDb } = data;
2100
+ const rangeDb = rawRangeDb === 0 ? 1 : rawRangeDb;
2101
+ let globalPixelOffset = 0;
2102
+ const binToFreq = (bin) => bin / frequencyBinCount * (sampleRate / 2);
2103
+ for (let canvasIdx = 0; canvasIdx < canvases2.length; canvasIdx++) {
2104
+ const canvas = canvases2[canvasIdx];
2105
+ if (!canvas) continue;
2106
+ const ctx = canvas.getContext("2d");
2107
+ if (!ctx) continue;
2108
+ const canvasWidth = canvas.width / devicePixelRatio;
2109
+ const canvasHeight = waveHeight;
2110
+ ctx.resetTransform();
2111
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2112
+ ctx.imageSmoothingEnabled = false;
2113
+ ctx.scale(devicePixelRatio, devicePixelRatio);
2114
+ const imgData = ctx.createImageData(canvasWidth, canvasHeight);
2115
+ const pixels = imgData.data;
2116
+ for (let x = 0; x < canvasWidth; x++) {
2117
+ const globalX = globalPixelOffset + x;
2118
+ const samplePos = globalX * samplesPerPixel;
2119
+ const frame = Math.floor(samplePos / hopSize);
2120
+ if (frame < 0 || frame >= frameCount) continue;
2121
+ const frameOffset = frame * frequencyBinCount;
2122
+ for (let y = 0; y < canvasHeight; y++) {
2123
+ const normalizedY = 1 - y / canvasHeight;
2124
+ let bin = Math.floor(normalizedY * frequencyBinCount);
2125
+ if (frequencyScaleFn) {
2126
+ let lo = 0;
2127
+ let hi = frequencyBinCount - 1;
2128
+ while (lo < hi) {
2129
+ const mid = lo + hi >> 1;
2130
+ const freq = binToFreq(mid);
2131
+ const scaled = scaleFn(freq, minFrequency, maxF);
2132
+ if (scaled < normalizedY) {
2133
+ lo = mid + 1;
2134
+ } else {
2135
+ hi = mid;
2136
+ }
2137
+ }
2138
+ bin = lo;
2139
+ }
2140
+ if (bin < 0 || bin >= frequencyBinCount) continue;
2141
+ const db = data.data[frameOffset + bin];
2142
+ const normalized = Math.max(0, Math.min(1, (db + rangeDb + gainDb) / rangeDb));
2143
+ const colorIdx = Math.floor(normalized * 255);
2144
+ const pixelIdx = (y * canvasWidth + x) * 4;
2145
+ pixels[pixelIdx] = lut[colorIdx * 3];
2146
+ pixels[pixelIdx + 1] = lut[colorIdx * 3 + 1];
2147
+ pixels[pixelIdx + 2] = lut[colorIdx * 3 + 2];
2148
+ pixels[pixelIdx + 3] = 255;
2149
+ }
2150
+ }
2151
+ ctx.resetTransform();
2152
+ ctx.putImageData(imgData, 0, 0);
2153
+ if (devicePixelRatio !== 1) {
2154
+ const tmpCanvas = document.createElement("canvas");
2155
+ tmpCanvas.width = canvasWidth;
2156
+ tmpCanvas.height = canvasHeight;
2157
+ const tmpCtx = tmpCanvas.getContext("2d");
2158
+ if (!tmpCtx) continue;
2159
+ tmpCtx.putImageData(imgData, 0, 0);
2160
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2161
+ ctx.imageSmoothingEnabled = false;
2162
+ ctx.drawImage(tmpCanvas, 0, 0, canvas.width, canvas.height);
2163
+ }
2164
+ globalPixelOffset += canvasWidth;
2165
+ }
2166
+ }, [isWorkerMode, data, length, waveHeight, devicePixelRatio, samplesPerPixel, lut, frequencyScaleFn, minFrequency, maxF, scaleFn]);
2167
+ let totalWidth = length;
2168
+ let canvasCount = 0;
2169
+ const canvases = [];
2170
+ while (totalWidth > 0) {
2171
+ const currentWidth = Math.min(totalWidth, MAX_CANVAS_WIDTH2);
2172
+ canvases.push(
2173
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2174
+ SpectrogramCanvas,
2175
+ {
2176
+ $cssWidth: currentWidth,
2177
+ width: currentWidth * devicePixelRatio,
2178
+ height: waveHeight * devicePixelRatio,
2179
+ $waveHeight: waveHeight,
2180
+ "data-index": canvasCount,
2181
+ ref: canvasRef
2182
+ },
2183
+ `${length}-${canvasCount}`
2184
+ )
2185
+ );
2186
+ totalWidth -= currentWidth;
2187
+ canvasCount++;
2188
+ }
2189
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Wrapper3, { $index: index, $cssWidth: length, $waveHeight: waveHeight, children: canvases });
2190
+ };
2191
+
2192
+ // src/components/SmartChannel.tsx
2193
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2194
+ var SmartChannel = ({
2195
+ isSelected,
2196
+ transparentBackground,
2197
+ renderMode = "waveform",
2198
+ spectrogramData,
2199
+ spectrogramColorLUT,
2200
+ samplesPerPixel: sppProp,
2201
+ spectrogramFrequencyScaleFn,
2202
+ spectrogramMinFrequency,
2203
+ spectrogramMaxFrequency,
2204
+ spectrogramWorkerApi,
2205
+ spectrogramClipId,
2206
+ spectrogramOnCanvasesReady,
2207
+ ...props
2208
+ }) => {
1997
2209
  const theme = useTheme2();
1998
- const { waveHeight, barWidth, barGap } = usePlaylistInfo();
2210
+ const { waveHeight, barWidth, barGap, samplesPerPixel: contextSpp } = usePlaylistInfo();
1999
2211
  const devicePixelRatio = useDevicePixelRatio();
2212
+ const samplesPerPixel = sppProp ?? contextSpp;
2000
2213
  const waveOutlineColor = isSelected && theme ? theme.selectedWaveOutlineColor : theme?.waveOutlineColor;
2001
2214
  const waveFillColor = isSelected && theme ? theme.selectedWaveFillColor : theme?.waveFillColor;
2002
2215
  const drawMode = theme?.waveformDrawMode || "inverted";
2003
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2216
+ const hasSpectrogram = spectrogramData || spectrogramWorkerApi;
2217
+ if (renderMode === "spectrogram" && hasSpectrogram) {
2218
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2219
+ SpectrogramChannel,
2220
+ {
2221
+ index: props.index,
2222
+ data: spectrogramData,
2223
+ length: props.length,
2224
+ waveHeight,
2225
+ devicePixelRatio,
2226
+ samplesPerPixel,
2227
+ colorLUT: spectrogramColorLUT,
2228
+ frequencyScaleFn: spectrogramFrequencyScaleFn,
2229
+ minFrequency: spectrogramMinFrequency,
2230
+ maxFrequency: spectrogramMaxFrequency,
2231
+ workerApi: spectrogramWorkerApi,
2232
+ clipId: spectrogramClipId,
2233
+ onCanvasesReady: spectrogramOnCanvasesReady
2234
+ }
2235
+ );
2236
+ }
2237
+ if (renderMode === "both" && hasSpectrogram) {
2238
+ const halfHeight = Math.floor(waveHeight / 2);
2239
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
2240
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2241
+ SpectrogramChannel,
2242
+ {
2243
+ index: props.index * 2,
2244
+ data: spectrogramData,
2245
+ length: props.length,
2246
+ waveHeight: halfHeight,
2247
+ devicePixelRatio,
2248
+ samplesPerPixel,
2249
+ colorLUT: spectrogramColorLUT,
2250
+ frequencyScaleFn: spectrogramFrequencyScaleFn,
2251
+ minFrequency: spectrogramMinFrequency,
2252
+ maxFrequency: spectrogramMaxFrequency,
2253
+ workerApi: spectrogramWorkerApi,
2254
+ clipId: spectrogramClipId,
2255
+ onCanvasesReady: spectrogramOnCanvasesReady
2256
+ }
2257
+ ),
2258
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: { position: "absolute", top: (props.index * 2 + 1) * halfHeight, width: props.length, height: halfHeight }, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2259
+ Channel,
2260
+ {
2261
+ ...props,
2262
+ ...theme,
2263
+ index: 0,
2264
+ waveOutlineColor,
2265
+ waveFillColor,
2266
+ waveHeight: halfHeight,
2267
+ devicePixelRatio,
2268
+ barWidth,
2269
+ barGap,
2270
+ transparentBackground,
2271
+ drawMode
2272
+ }
2273
+ ) })
2274
+ ] });
2275
+ }
2276
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2004
2277
  Channel,
2005
2278
  {
2006
2279
  ...props,
@@ -2017,12 +2290,112 @@ var SmartChannel = ({ isSelected, transparentBackground, ...props }) => {
2017
2290
  );
2018
2291
  };
2019
2292
 
2020
- // src/components/SmartScale.tsx
2293
+ // src/components/SpectrogramLabels.tsx
2021
2294
  var import_react13 = require("react");
2295
+ var import_styled_components21 = __toESM(require("styled-components"));
2296
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2297
+ var LABELS_WIDTH = 72;
2298
+ var LabelsStickyWrapper = import_styled_components21.default.div`
2299
+ position: sticky;
2300
+ left: 0;
2301
+ z-index: 101;
2302
+ pointer-events: none;
2303
+ height: 0;
2304
+ width: 0;
2305
+ overflow: visible;
2306
+ `;
2307
+ function getFrequencyLabels(minF, maxF, height) {
2308
+ const allCandidates = [
2309
+ 20,
2310
+ 50,
2311
+ 100,
2312
+ 200,
2313
+ 500,
2314
+ 1e3,
2315
+ 2e3,
2316
+ 3e3,
2317
+ 4e3,
2318
+ 5e3,
2319
+ 8e3,
2320
+ 1e4,
2321
+ 12e3,
2322
+ 16e3,
2323
+ 2e4
2324
+ ];
2325
+ const inRange = allCandidates.filter((f) => f >= minF && f <= maxF);
2326
+ const maxLabels = Math.max(2, Math.floor(height / 20));
2327
+ if (inRange.length <= maxLabels) return inRange;
2328
+ const step = (inRange.length - 1) / (maxLabels - 1);
2329
+ const result = [];
2330
+ for (let i = 0; i < maxLabels; i++) {
2331
+ result.push(inRange[Math.round(i * step)]);
2332
+ }
2333
+ return result;
2334
+ }
2335
+ var SpectrogramLabels = ({
2336
+ waveHeight,
2337
+ numChannels,
2338
+ frequencyScaleFn,
2339
+ minFrequency,
2340
+ maxFrequency,
2341
+ labelsColor = "#ccc",
2342
+ labelsBackground = "rgba(0,0,0,0.6)",
2343
+ renderMode = "spectrogram",
2344
+ hasClipHeaders = false
2345
+ }) => {
2346
+ const canvasRef = (0, import_react13.useRef)(null);
2347
+ const devicePixelRatio = useDevicePixelRatio();
2348
+ const spectrogramHeight = renderMode === "both" ? Math.floor(waveHeight / 2) : waveHeight;
2349
+ const totalHeight = numChannels * waveHeight;
2350
+ const clipHeaderOffset = hasClipHeaders ? 22 : 0;
2351
+ (0, import_react13.useLayoutEffect)(() => {
2352
+ const canvas = canvasRef.current;
2353
+ if (!canvas) return;
2354
+ const ctx = canvas.getContext("2d");
2355
+ if (!ctx) return;
2356
+ ctx.resetTransform();
2357
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2358
+ ctx.scale(devicePixelRatio, devicePixelRatio);
2359
+ const labelFreqs = getFrequencyLabels(minFrequency, maxFrequency, spectrogramHeight);
2360
+ for (let ch = 0; ch < numChannels; ch++) {
2361
+ const channelTop = ch * waveHeight + clipHeaderOffset;
2362
+ ctx.font = "11px monospace";
2363
+ ctx.textBaseline = "middle";
2364
+ for (const freq of labelFreqs) {
2365
+ const normalized = frequencyScaleFn(freq, minFrequency, maxFrequency);
2366
+ if (normalized < 0 || normalized > 1) continue;
2367
+ const y = channelTop + spectrogramHeight * (1 - normalized);
2368
+ const text = freq >= 1e3 ? `${(freq / 1e3).toFixed(1)}k` : `${freq} Hz`;
2369
+ const metrics = ctx.measureText(text);
2370
+ const padding = 3;
2371
+ ctx.fillStyle = labelsBackground;
2372
+ ctx.fillRect(0, y - 7, metrics.width + padding * 2, 14);
2373
+ ctx.fillStyle = labelsColor;
2374
+ ctx.fillText(text, padding, y);
2375
+ }
2376
+ }
2377
+ }, [waveHeight, numChannels, frequencyScaleFn, minFrequency, maxFrequency, labelsColor, labelsBackground, devicePixelRatio, spectrogramHeight, clipHeaderOffset]);
2378
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(LabelsStickyWrapper, { $height: totalHeight + clipHeaderOffset, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2379
+ "canvas",
2380
+ {
2381
+ ref: canvasRef,
2382
+ width: LABELS_WIDTH * devicePixelRatio,
2383
+ height: (totalHeight + clipHeaderOffset) * devicePixelRatio,
2384
+ style: {
2385
+ width: LABELS_WIDTH,
2386
+ height: totalHeight + clipHeaderOffset,
2387
+ pointerEvents: "none"
2388
+ }
2389
+ }
2390
+ ) });
2391
+ };
2392
+
2393
+ // src/components/SmartScale.tsx
2394
+ var import_react15 = require("react");
2022
2395
 
2023
2396
  // src/components/TimeScale.tsx
2024
- var import_react12 = __toESM(require("react"));
2025
- var import_styled_components20 = __toESM(require("styled-components"));
2397
+ var import_react14 = __toESM(require("react"));
2398
+ var import_styled_components22 = __toESM(require("styled-components"));
2026
2399
 
2027
2400
  // src/utils/conversions.ts
2028
2401
  function samplesToSeconds(samples, sampleRate) {
@@ -2045,14 +2418,14 @@ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
2045
2418
  }
2046
2419
 
2047
2420
  // src/components/TimeScale.tsx
2048
- var import_jsx_runtime19 = require("react/jsx-runtime");
2421
+ var import_jsx_runtime21 = require("react/jsx-runtime");
2049
2422
  function formatTime2(milliseconds) {
2050
2423
  const seconds = Math.floor(milliseconds / 1e3);
2051
2424
  const s = seconds % 60;
2052
2425
  const m = (seconds - s) / 60;
2053
2426
  return `${m}:${String(s).padStart(2, "0")}`;
2054
2427
  }
2055
- var PlaylistTimeScaleScroll = import_styled_components20.default.div.attrs((props) => ({
2428
+ var PlaylistTimeScaleScroll = import_styled_components22.default.div.attrs((props) => ({
2056
2429
  style: {
2057
2430
  width: `${props.$cssWidth}px`,
2058
2431
  marginLeft: `${props.$controlWidth}px`,
@@ -2064,7 +2437,7 @@ var PlaylistTimeScaleScroll = import_styled_components20.default.div.attrs((prop
2064
2437
  border-bottom: 1px solid ${(props) => props.theme.timeColor};
2065
2438
  box-sizing: border-box;
2066
2439
  `;
2067
- var TimeTicks = import_styled_components20.default.canvas.attrs((props) => ({
2440
+ var TimeTicks = import_styled_components22.default.canvas.attrs((props) => ({
2068
2441
  style: {
2069
2442
  width: `${props.$cssWidth}px`,
2070
2443
  height: `${props.$timeScaleHeight}px`
@@ -2075,7 +2448,7 @@ var TimeTicks = import_styled_components20.default.canvas.attrs((props) => ({
2075
2448
  right: 0;
2076
2449
  bottom: 0;
2077
2450
  `;
2078
- var TimeStamp = import_styled_components20.default.div.attrs((props) => ({
2451
+ var TimeStamp = import_styled_components22.default.div.attrs((props) => ({
2079
2452
  style: {
2080
2453
  left: `${props.$left + 4}px`
2081
2454
  // Offset 4px to the right of the tick
@@ -2097,15 +2470,15 @@ var TimeScale = (props) => {
2097
2470
  } = props;
2098
2471
  const canvasInfo = /* @__PURE__ */ new Map();
2099
2472
  const timeMarkers = [];
2100
- const canvasRef = (0, import_react12.useRef)(null);
2473
+ const canvasRef = (0, import_react14.useRef)(null);
2101
2474
  const {
2102
2475
  sampleRate,
2103
2476
  samplesPerPixel,
2104
2477
  timeScaleHeight,
2105
2478
  controls: { show: showControls, width: controlWidth }
2106
- } = (0, import_react12.useContext)(PlaylistInfoContext);
2479
+ } = (0, import_react14.useContext)(PlaylistInfoContext);
2107
2480
  const devicePixelRatio = useDevicePixelRatio();
2108
- (0, import_react12.useEffect)(() => {
2481
+ (0, import_react14.useEffect)(() => {
2109
2482
  if (canvasRef.current !== null) {
2110
2483
  const canvas = canvasRef.current;
2111
2484
  const ctx = canvas.getContext("2d");
@@ -2139,7 +2512,7 @@ var TimeScale = (props) => {
2139
2512
  if (counter % marker === 0) {
2140
2513
  const timeMs = counter;
2141
2514
  const timestamp = formatTime2(timeMs);
2142
- const timestampContent = renderTimestamp ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react12.default.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2515
+ const timestampContent = renderTimestamp ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react14.default.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TimeStamp, { $left: pix, children: timestamp }, timestamp);
2143
2516
  timeMarkers.push(timestampContent);
2144
2517
  canvasInfo.set(pix, timeScaleHeight);
2145
2518
  } else if (counter % bigStep === 0) {
@@ -2149,7 +2522,7 @@ var TimeScale = (props) => {
2149
2522
  }
2150
2523
  counter += secondStep;
2151
2524
  }
2152
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
2525
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
2153
2526
  PlaylistTimeScaleScroll,
2154
2527
  {
2155
2528
  $cssWidth: widthX,
@@ -2157,7 +2530,7 @@ var TimeScale = (props) => {
2157
2530
  $timeScaleHeight: timeScaleHeight,
2158
2531
  children: [
2159
2532
  timeMarkers,
2160
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2533
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2161
2534
  TimeTicks,
2162
2535
  {
2163
2536
  $cssWidth: widthX,
@@ -2171,10 +2544,10 @@ var TimeScale = (props) => {
2171
2544
  }
2172
2545
  );
2173
2546
  };
2174
- var StyledTimeScale = (0, import_styled_components20.withTheme)(TimeScale);
2547
+ var StyledTimeScale = (0, import_styled_components22.withTheme)(TimeScale);
2175
2548
 
2176
2549
  // src/components/SmartScale.tsx
2177
- var import_jsx_runtime20 = require("react/jsx-runtime");
2550
+ var import_jsx_runtime22 = require("react/jsx-runtime");
2178
2551
  var timeinfo = /* @__PURE__ */ new Map([
2179
2552
  [
2180
2553
  700,
@@ -2247,24 +2620,25 @@ function getScaleInfo(samplesPerPixel) {
2247
2620
  }
2248
2621
  return config;
2249
2622
  }
2250
- var SmartScale = () => {
2251
- const { samplesPerPixel, duration } = (0, import_react13.useContext)(PlaylistInfoContext);
2623
+ var SmartScale = ({ renderTimestamp }) => {
2624
+ const { samplesPerPixel, duration } = (0, import_react15.useContext)(PlaylistInfoContext);
2252
2625
  let config = getScaleInfo(samplesPerPixel);
2253
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2626
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2254
2627
  StyledTimeScale,
2255
2628
  {
2256
2629
  marker: config.marker,
2257
2630
  bigStep: config.bigStep,
2258
2631
  secondStep: config.smallStep,
2259
- duration
2632
+ duration,
2633
+ renderTimestamp
2260
2634
  }
2261
2635
  );
2262
2636
  };
2263
2637
 
2264
2638
  // src/components/TimeFormatSelect.tsx
2265
- var import_styled_components21 = __toESM(require("styled-components"));
2266
- var import_jsx_runtime21 = require("react/jsx-runtime");
2267
- var SelectWrapper = import_styled_components21.default.div`
2639
+ var import_styled_components23 = __toESM(require("styled-components"));
2640
+ var import_jsx_runtime23 = require("react/jsx-runtime");
2641
+ var SelectWrapper = import_styled_components23.default.div`
2268
2642
  display: inline-flex;
2269
2643
  align-items: center;
2270
2644
  gap: 0.5rem;
@@ -2286,7 +2660,7 @@ var TimeFormatSelect = ({
2286
2660
  const handleChange = (e) => {
2287
2661
  onChange(e.target.value);
2288
2662
  };
2289
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectWrapper, { className, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2663
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(SelectWrapper, { className, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2290
2664
  BaseSelect,
2291
2665
  {
2292
2666
  className: "time-format",
@@ -2294,15 +2668,15 @@ var TimeFormatSelect = ({
2294
2668
  onChange: handleChange,
2295
2669
  disabled,
2296
2670
  "aria-label": "Time format selection",
2297
- children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("option", { value: option.value, children: option.label }, option.value))
2671
+ children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("option", { value: option.value, children: option.label }, option.value))
2298
2672
  }
2299
2673
  ) });
2300
2674
  };
2301
2675
 
2302
2676
  // src/components/Track.tsx
2303
- var import_styled_components22 = __toESM(require("styled-components"));
2304
- var import_jsx_runtime22 = require("react/jsx-runtime");
2305
- var Container = import_styled_components22.default.div.attrs((props) => ({
2677
+ var import_styled_components24 = __toESM(require("styled-components"));
2678
+ var import_jsx_runtime24 = require("react/jsx-runtime");
2679
+ var Container = import_styled_components24.default.div.attrs((props) => ({
2306
2680
  style: {
2307
2681
  height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
2308
2682
  }
@@ -2311,7 +2685,7 @@ var Container = import_styled_components22.default.div.attrs((props) => ({
2311
2685
  display: flex;
2312
2686
  ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
2313
2687
  `;
2314
- var ChannelContainer = import_styled_components22.default.div.attrs((props) => ({
2688
+ var ChannelContainer = import_styled_components24.default.div.attrs((props) => ({
2315
2689
  style: {
2316
2690
  paddingLeft: `${props.$offset || 0}px`
2317
2691
  }
@@ -2320,13 +2694,13 @@ var ChannelContainer = import_styled_components22.default.div.attrs((props) => (
2320
2694
  background: ${(props) => props.$backgroundColor || "transparent"};
2321
2695
  flex: 1;
2322
2696
  `;
2323
- var ControlsWrapper = import_styled_components22.default.div.attrs((props) => ({
2697
+ var ControlsWrapper = import_styled_components24.default.div.attrs((props) => ({
2324
2698
  style: {
2325
2699
  width: `${props.$controlWidth}px`
2326
2700
  }
2327
2701
  }))`
2328
2702
  position: sticky;
2329
- z-index: 101; /* Above waveform content, below Docusaurus navbar (z-index: 200) */
2703
+ z-index: 102; /* Above waveform content and spectrogram labels (101), below Docusaurus navbar (200) */
2330
2704
  left: 0;
2331
2705
  height: 100%;
2332
2706
  flex-shrink: 0;
@@ -2356,7 +2730,7 @@ var Track = ({
2356
2730
  controls: { show, width: controlWidth }
2357
2731
  } = usePlaylistInfo();
2358
2732
  const controls = useTrackControls();
2359
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2733
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
2360
2734
  Container,
2361
2735
  {
2362
2736
  $numChannels: numChannels,
@@ -2367,7 +2741,7 @@ var Track = ({
2367
2741
  $hasClipHeaders: hasClipHeaders,
2368
2742
  $isSelected: isSelected,
2369
2743
  children: [
2370
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2744
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2371
2745
  ControlsWrapper,
2372
2746
  {
2373
2747
  $controlWidth: show ? controlWidth : 0,
@@ -2375,7 +2749,7 @@ var Track = ({
2375
2749
  children: controls
2376
2750
  }
2377
2751
  ),
2378
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2752
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2379
2753
  ChannelContainer,
2380
2754
  {
2381
2755
  $controlWidth: show ? controlWidth : 0,
@@ -2392,8 +2766,8 @@ var Track = ({
2392
2766
  };
2393
2767
 
2394
2768
  // src/components/TrackControls/Button.tsx
2395
- var import_styled_components23 = __toESM(require("styled-components"));
2396
- var Button = import_styled_components23.default.button.attrs({
2769
+ var import_styled_components25 = __toESM(require("styled-components"));
2770
+ var Button = import_styled_components25.default.button.attrs({
2397
2771
  type: "button"
2398
2772
  })`
2399
2773
  display: inline-block;
@@ -2465,8 +2839,8 @@ var Button = import_styled_components23.default.button.attrs({
2465
2839
  `;
2466
2840
 
2467
2841
  // src/components/TrackControls/ButtonGroup.tsx
2468
- var import_styled_components24 = __toESM(require("styled-components"));
2469
- var ButtonGroup = import_styled_components24.default.div`
2842
+ var import_styled_components26 = __toESM(require("styled-components"));
2843
+ var ButtonGroup = import_styled_components26.default.div`
2470
2844
  margin-bottom: 0.3rem;
2471
2845
 
2472
2846
  button:not(:first-child) {
@@ -2480,9 +2854,39 @@ var ButtonGroup = import_styled_components24.default.div`
2480
2854
  }
2481
2855
  `;
2482
2856
 
2857
+ // src/components/TrackControls/CloseButton.tsx
2858
+ var import_styled_components27 = __toESM(require("styled-components"));
2859
+ var import_react16 = require("@phosphor-icons/react");
2860
+ var import_jsx_runtime25 = require("react/jsx-runtime");
2861
+ var StyledCloseButton = import_styled_components27.default.button`
2862
+ position: absolute;
2863
+ left: 0;
2864
+ top: 0;
2865
+ border: none;
2866
+ background: transparent;
2867
+ color: inherit;
2868
+ cursor: pointer;
2869
+ font-size: 16px;
2870
+ padding: 2px 4px;
2871
+ display: flex;
2872
+ align-items: center;
2873
+ justify-content: center;
2874
+ opacity: 0.7;
2875
+ transition: opacity 0.15s, color 0.15s;
2876
+
2877
+ &:hover {
2878
+ opacity: 1;
2879
+ color: #dc3545;
2880
+ }
2881
+ `;
2882
+ var CloseButton = ({
2883
+ onClick,
2884
+ title = "Remove track"
2885
+ }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(StyledCloseButton, { onClick, title, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react16.X, { size: 12, weight: "bold" }) });
2886
+
2483
2887
  // src/components/TrackControls/Controls.tsx
2484
- var import_styled_components25 = __toESM(require("styled-components"));
2485
- var Controls = import_styled_components25.default.div`
2888
+ var import_styled_components28 = __toESM(require("styled-components"));
2889
+ var Controls = import_styled_components28.default.div`
2486
2890
  background: transparent;
2487
2891
  width: 100%;
2488
2892
  height: 100%;
@@ -2498,8 +2902,8 @@ var Controls = import_styled_components25.default.div`
2498
2902
  `;
2499
2903
 
2500
2904
  // src/components/TrackControls/Header.tsx
2501
- var import_styled_components26 = __toESM(require("styled-components"));
2502
- var Header = import_styled_components26.default.header`
2905
+ var import_styled_components29 = __toESM(require("styled-components"));
2906
+ var Header = import_styled_components29.default.header`
2503
2907
  overflow: hidden;
2504
2908
  height: 26px;
2505
2909
  width: 100%;
@@ -2513,23 +2917,28 @@ var Header = import_styled_components26.default.header`
2513
2917
  `;
2514
2918
 
2515
2919
  // src/components/TrackControls/VolumeDownIcon.tsx
2516
- var import_react14 = require("@phosphor-icons/react");
2517
- var import_jsx_runtime23 = require("react/jsx-runtime");
2518
- var VolumeDownIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react14.SpeakerLowIcon, { weight: "light", ...props });
2920
+ var import_react17 = require("@phosphor-icons/react");
2921
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2922
+ var VolumeDownIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react17.SpeakerLowIcon, { weight: "light", ...props });
2519
2923
 
2520
2924
  // src/components/TrackControls/VolumeUpIcon.tsx
2521
- var import_react15 = require("@phosphor-icons/react");
2522
- var import_jsx_runtime24 = require("react/jsx-runtime");
2523
- var VolumeUpIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react15.SpeakerHighIcon, { weight: "light", ...props });
2925
+ var import_react18 = require("@phosphor-icons/react");
2926
+ var import_jsx_runtime27 = require("react/jsx-runtime");
2927
+ var VolumeUpIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react18.SpeakerHighIcon, { weight: "light", ...props });
2524
2928
 
2525
2929
  // src/components/TrackControls/TrashIcon.tsx
2526
- var import_react16 = require("@phosphor-icons/react");
2527
- var import_jsx_runtime25 = require("react/jsx-runtime");
2528
- var TrashIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react16.TrashIcon, { weight: "light", ...props });
2930
+ var import_react19 = require("@phosphor-icons/react");
2931
+ var import_jsx_runtime28 = require("react/jsx-runtime");
2932
+ var TrashIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react19.TrashIcon, { weight: "light", ...props });
2933
+
2934
+ // src/components/TrackControls/DotsIcon.tsx
2935
+ var import_react20 = require("@phosphor-icons/react");
2936
+ var import_jsx_runtime29 = require("react/jsx-runtime");
2937
+ var DotsIcon = (props) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react20.DotsThreeIcon, { weight: "bold", ...props });
2529
2938
 
2530
2939
  // src/components/TrackControls/Slider.tsx
2531
- var import_styled_components27 = __toESM(require("styled-components"));
2532
- var Slider = (0, import_styled_components27.default)(BaseSlider)`
2940
+ var import_styled_components30 = __toESM(require("styled-components"));
2941
+ var Slider = (0, import_styled_components30.default)(BaseSlider)`
2533
2942
  width: 75%;
2534
2943
  height: 5px;
2535
2944
  background: ${(props) => props.theme.sliderTrackColor};
@@ -2581,8 +2990,8 @@ var Slider = (0, import_styled_components27.default)(BaseSlider)`
2581
2990
  `;
2582
2991
 
2583
2992
  // src/components/TrackControls/SliderWrapper.tsx
2584
- var import_styled_components28 = __toESM(require("styled-components"));
2585
- var SliderWrapper = import_styled_components28.default.label`
2993
+ var import_styled_components31 = __toESM(require("styled-components"));
2994
+ var SliderWrapper = import_styled_components31.default.label`
2586
2995
  width: 100%;
2587
2996
  display: flex;
2588
2997
  justify-content: space-between;
@@ -2592,113 +3001,108 @@ var SliderWrapper = import_styled_components28.default.label`
2592
3001
  font-size: 14px;
2593
3002
  `;
2594
3003
 
2595
- // src/components/TrackControlsWithDelete.tsx
2596
- var import_styled_components29 = __toESM(require("styled-components"));
2597
- var import_jsx_runtime26 = require("react/jsx-runtime");
2598
- var HeaderContainer2 = import_styled_components29.default.div`
2599
- display: flex;
2600
- align-items: center;
2601
- gap: 0.25rem;
2602
- padding: 0.5rem 0.5rem 0.25rem 0.5rem;
2603
- `;
2604
- var TrackNameSpan = import_styled_components29.default.span`
2605
- flex: 1;
2606
- font-weight: 600;
2607
- font-size: 0.875rem;
2608
- overflow: hidden;
2609
- text-overflow: ellipsis;
2610
- white-space: nowrap;
2611
- margin: 0 0.25rem;
3004
+ // src/components/TrackMenu.tsx
3005
+ var import_react21 = __toESM(require("react"));
3006
+ var import_react_dom = require("react-dom");
3007
+ var import_styled_components32 = __toESM(require("styled-components"));
3008
+ var import_jsx_runtime30 = require("react/jsx-runtime");
3009
+ var MenuContainer = import_styled_components32.default.div`
3010
+ position: relative;
3011
+ display: inline-block;
2612
3012
  `;
2613
- var DeleteIconButton = import_styled_components29.default.button`
3013
+ var MenuButton = import_styled_components32.default.button`
3014
+ background: none;
3015
+ border: none;
3016
+ cursor: pointer;
3017
+ padding: 2px 4px;
2614
3018
  display: flex;
2615
3019
  align-items: center;
2616
3020
  justify-content: center;
2617
- width: 20px;
2618
- height: 20px;
2619
- padding: 0;
2620
- border: none;
2621
- background: transparent;
2622
- color: #999;
2623
- cursor: pointer;
2624
- font-size: 16px;
2625
- line-height: 1;
2626
- border-radius: 3px;
2627
- transition: all 0.2s ease-in-out;
2628
- flex-shrink: 0;
3021
+ color: inherit;
3022
+ opacity: 0.7;
2629
3023
 
2630
3024
  &:hover {
2631
- background: #dc3545;
2632
- color: white;
2633
- }
2634
-
2635
- &:active {
2636
- transform: scale(0.9);
3025
+ opacity: 1;
2637
3026
  }
2638
3027
  `;
2639
- var TrackControlsWithDelete = ({
2640
- trackName,
2641
- muted,
2642
- soloed,
2643
- volume,
2644
- pan,
2645
- onMuteChange,
2646
- onSoloChange,
2647
- onVolumeChange,
2648
- onPanChange,
2649
- onDelete
3028
+ var Dropdown = import_styled_components32.default.div`
3029
+ position: fixed;
3030
+ top: ${(p) => p.$top}px;
3031
+ left: ${(p) => p.$left}px;
3032
+ z-index: 10000;
3033
+ background: ${(p) => p.theme.timescaleBackgroundColor ?? "#222"};
3034
+ color: ${(p) => p.theme.textColor ?? "inherit"};
3035
+ border: 1px solid rgba(128, 128, 128, 0.4);
3036
+ border-radius: 6px;
3037
+ padding: 0.5rem 0;
3038
+ min-width: 180px;
3039
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3040
+ `;
3041
+ var Divider = import_styled_components32.default.hr`
3042
+ border: none;
3043
+ border-top: 1px solid rgba(128, 128, 128, 0.3);
3044
+ margin: 0.35rem 0;
3045
+ `;
3046
+ var TrackMenu = ({
3047
+ items: itemsProp
2650
3048
  }) => {
2651
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(Controls, { children: [
2652
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(HeaderContainer2, { children: [
2653
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(DeleteIconButton, { onClick: onDelete, title: "Delete track", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrashIcon, {}) }),
2654
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackNameSpan, { children: trackName })
2655
- ] }),
2656
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(ButtonGroup, { children: [
2657
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2658
- Button,
2659
- {
2660
- $variant: muted ? "danger" : "outline",
2661
- onClick: () => onMuteChange(!muted),
2662
- children: "Mute"
2663
- }
2664
- ),
2665
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2666
- Button,
2667
- {
2668
- $variant: soloed ? "info" : "outline",
2669
- onClick: () => onSoloChange(!soloed),
2670
- children: "Solo"
2671
- }
2672
- )
2673
- ] }),
2674
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(SliderWrapper, { children: [
2675
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VolumeDownIcon, {}),
2676
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2677
- Slider,
2678
- {
2679
- min: "0",
2680
- max: "1",
2681
- step: "0.01",
2682
- value: volume,
2683
- onChange: (e) => onVolumeChange(parseFloat(e.target.value))
2684
- }
2685
- ),
2686
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VolumeUpIcon, {})
2687
- ] }),
2688
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(SliderWrapper, { children: [
2689
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: "L" }),
2690
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2691
- Slider,
3049
+ const [open, setOpen] = (0, import_react21.useState)(false);
3050
+ const close = () => setOpen(false);
3051
+ const items = typeof itemsProp === "function" ? itemsProp(close) : itemsProp;
3052
+ const [dropdownPos, setDropdownPos] = (0, import_react21.useState)({ top: 0, left: 0 });
3053
+ const buttonRef = (0, import_react21.useRef)(null);
3054
+ const dropdownRef = (0, import_react21.useRef)(null);
3055
+ (0, import_react21.useEffect)(() => {
3056
+ if (open && buttonRef.current) {
3057
+ const rect = buttonRef.current.getBoundingClientRect();
3058
+ setDropdownPos({
3059
+ top: rect.bottom + 2,
3060
+ left: Math.max(0, rect.right - 180)
3061
+ });
3062
+ }
3063
+ }, [open]);
3064
+ (0, import_react21.useEffect)(() => {
3065
+ if (!open) return;
3066
+ const handleClick = (e) => {
3067
+ const target = e.target;
3068
+ if (buttonRef.current && !buttonRef.current.contains(target) && dropdownRef.current && !dropdownRef.current.contains(target)) {
3069
+ setOpen(false);
3070
+ }
3071
+ };
3072
+ document.addEventListener("mousedown", handleClick);
3073
+ return () => document.removeEventListener("mousedown", handleClick);
3074
+ }, [open]);
3075
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(MenuContainer, { children: [
3076
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3077
+ MenuButton,
3078
+ {
3079
+ ref: buttonRef,
3080
+ onClick: (e) => {
3081
+ e.stopPropagation();
3082
+ setOpen((prev) => !prev);
3083
+ },
3084
+ onMouseDown: (e) => e.stopPropagation(),
3085
+ title: "Track menu",
3086
+ "aria-label": "Track menu",
3087
+ children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(DotsIcon, { size: 16 })
3088
+ }
3089
+ ),
3090
+ open && typeof document !== "undefined" && (0, import_react_dom.createPortal)(
3091
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3092
+ Dropdown,
2692
3093
  {
2693
- min: "-1",
2694
- max: "1",
2695
- step: "0.01",
2696
- value: pan,
2697
- onChange: (e) => onPanChange(parseFloat(e.target.value))
3094
+ ref: dropdownRef,
3095
+ $top: dropdownPos.top,
3096
+ $left: dropdownPos.left,
3097
+ onMouseDown: (e) => e.stopPropagation(),
3098
+ children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_react21.default.Fragment, { children: [
3099
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Divider, {}),
3100
+ item.content
3101
+ ] }, item.id))
2698
3102
  }
2699
3103
  ),
2700
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: "R" })
2701
- ] })
3104
+ document.body
3105
+ )
2702
3106
  ] });
2703
3107
  };
2704
3108
  // Annotate the CommonJS export names for ESM import in node:
@@ -2724,8 +3128,10 @@ var TrackControlsWithDelete = ({
2724
3128
  ClipBoundary,
2725
3129
  ClipHeader,
2726
3130
  ClipHeaderPresentational,
3131
+ CloseButton,
2727
3132
  Controls,
2728
3133
  DevicePixelRatioProvider,
3134
+ DotsIcon,
2729
3135
  FadeOverlay,
2730
3136
  Header,
2731
3137
  InlineLabel,
@@ -2744,6 +3150,8 @@ var TrackControlsWithDelete = ({
2744
3150
  SliderWrapper,
2745
3151
  SmartChannel,
2746
3152
  SmartScale,
3153
+ SpectrogramChannel,
3154
+ SpectrogramLabels,
2747
3155
  StyledPlaylist,
2748
3156
  StyledTimeScale,
2749
3157
  TimeFormatSelect,
@@ -2752,7 +3160,7 @@ var TrackControlsWithDelete = ({
2752
3160
  TimescaleLoopRegion,
2753
3161
  Track,
2754
3162
  TrackControlsContext,
2755
- TrackControlsWithDelete,
3163
+ TrackMenu,
2756
3164
  TrashIcon,
2757
3165
  VolumeDownIcon,
2758
3166
  VolumeUpIcon,