@waveform-playlist/ui-components 5.2.0 → 5.3.1

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