@proteos/ui 0.77.0 → 0.78.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.
Files changed (32) hide show
  1. package/dist/index.cjs +991 -5
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +592 -2
  4. package/dist/index.d.ts +592 -2
  5. package/dist/index.js +969 -9
  6. package/dist/index.js.map +1 -1
  7. package/package.json +3 -2
  8. package/src/components/data-table/data-table.scroll-restore.test.tsx +97 -0
  9. package/src/components/data-table/data-table.test.tsx +22 -2
  10. package/src/components/data-table/data-table.tsx +31 -1
  11. package/src/components/frame-strip/frame-strip.stories.tsx +61 -0
  12. package/src/components/frame-strip/frame-strip.test.tsx +112 -0
  13. package/src/components/frame-strip/frame-strip.tsx +131 -0
  14. package/src/components/frame-strip/index.ts +2 -0
  15. package/src/components/media-player/index.ts +22 -0
  16. package/src/components/media-player/media-controls.tsx +145 -0
  17. package/src/components/media-player/media-markers.tsx +108 -0
  18. package/src/components/media-player/media-peaks.test.ts +68 -0
  19. package/src/components/media-player/media-peaks.ts +78 -0
  20. package/src/components/media-player/media-player.stories.tsx +243 -0
  21. package/src/components/media-player/media-player.test.tsx +318 -0
  22. package/src/components/media-player/media-player.tsx +334 -0
  23. package/src/components/media-player/media-time.ts +27 -0
  24. package/src/components/media-player/media-timeline.test.tsx +133 -0
  25. package/src/components/media-player/media-timeline.tsx +78 -0
  26. package/src/components/media-player/media-video-stage.tsx +113 -0
  27. package/src/components/media-player/media-waveform.tsx +330 -0
  28. package/src/components/slider/index.ts +2 -0
  29. package/src/components/slider/slider.stories.tsx +32 -0
  30. package/src/components/slider/slider.test.tsx +35 -0
  31. package/src/components/slider/slider.tsx +89 -0
  32. package/src/index.ts +3 -0
package/dist/index.cjs CHANGED
@@ -126,12 +126,12 @@ function formatFileSize(bytes) {
126
126
  if (bytes < 1024) return `${bytes} B`;
127
127
  const units = ["KB", "MB", "GB", "TB"];
128
128
  let size = bytes / 1024;
129
- let unit = 0;
130
- while (size >= 1024 && unit < units.length - 1) {
129
+ let unit2 = 0;
130
+ while (size >= 1024 && unit2 < units.length - 1) {
131
131
  size /= 1024;
132
- unit += 1;
132
+ unit2 += 1;
133
133
  }
134
- return `${size < 10 ? size.toFixed(1) : Math.round(size)} ${units[unit]}`;
134
+ return `${size < 10 ? size.toFixed(1) : Math.round(size)} ${units[unit2]}`;
135
135
  }
136
136
  function FileField({
137
137
  value,
@@ -3998,13 +3998,35 @@ function DataTable({
3998
3998
  setListOffset((prev) => prev === top ? prev : top);
3999
3999
  });
4000
4000
  const headingHeight = groupHeaderHeight ?? rowHeight;
4001
+ const [isScrollerAttached, setIsScrollerAttached] = react.useState(false);
4002
+ react.useLayoutEffect(() => setIsScrollerAttached(true), []);
4001
4003
  const virtualizer = reactVirtual.useVirtualizer({
4004
+ enabled: initialScrollOffset === void 0 || isScrollerAttached,
4002
4005
  count: listItems.length,
4003
4006
  getScrollElement: () => scrollerRef.current,
4004
4007
  estimateSize: (index) => listItems[index]?.kind === "group" ? headingHeight : rowHeight,
4005
4008
  overscan,
4006
4009
  scrollMargin: listOffset,
4007
- ...initialScrollOffset !== void 0 ? { initialOffset: initialScrollOffset } : {}
4010
+ // Read once, with the scroller attached (see `enabled`). Clamp the restored
4011
+ // offset to what the scroller will be able to reach: while the skeleton
4012
+ // shows, or when the list shrank since the visit, the browser clamps
4013
+ // scrollTop WITHOUT firing a scroll event, and the virtualizer — which only
4014
+ // re-reads its offset on scroll events — would sit past the rows and paint
4015
+ // empty. The reach comes from the row data, not `scrollHeight`: the disabled
4016
+ // first render committed an empty spacer, so the DOM can't report it yet.
4017
+ ...initialScrollOffset !== void 0 ? {
4018
+ initialOffset: () => {
4019
+ const el = scrollerRef.current;
4020
+ if (!el) return 0;
4021
+ const estimatedSize = listItems.reduce(
4022
+ (sum, item) => sum + (item.kind === "group" ? headingHeight : rowHeight),
4023
+ 0
4024
+ );
4025
+ const listTop = listRef.current?.offsetTop ?? 0;
4026
+ const reachable = listTop + estimatedSize - el.clientHeight;
4027
+ return Math.max(0, Math.min(initialScrollOffset, reachable));
4028
+ }
4029
+ } : {}
4008
4030
  });
4009
4031
  const sentinelRef = useInfiniteScrollSentinel({
4010
4032
  onIntersect: () => {
@@ -6029,6 +6051,103 @@ function FileViewer({
6029
6051
  const skeleton = /* @__PURE__ */ jsxRuntime.jsx(chunk4ACELYEP_cjs.Skeleton, { "aria-label": `Loading preview of ${name}`, className: "h-full w-full rounded-none" });
6030
6052
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunk4ACELYEP_cjs.cn(fileViewerStyles(), className), style: { height: heightPx }, children: !src ? skeleton : kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(ImageView, { name, src, ...onDownload ? { onDownload } : {} }) : kind === "video" ? /* @__PURE__ */ jsxRuntime.jsx(VideoView, { name, src }) : /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: skeleton, children: /* @__PURE__ */ jsxRuntime.jsx(PdfView, { name, src, ...onDownload ? { onDownload } : {} }) }) });
6031
6053
  }
6054
+
6055
+ // src/components/media-player/media-time.ts
6056
+ var pad = (value) => String(value).padStart(2, "0");
6057
+ function formatMediaTime(seconds) {
6058
+ const total = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
6059
+ const hours = Math.floor(total / 3600);
6060
+ const minutes = Math.floor(total % 3600 / 60);
6061
+ const rest = total % 60;
6062
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(rest)}` : `${pad(minutes)}:${pad(rest)}`;
6063
+ }
6064
+ var unit = (value, name) => `${value} ${name}${value === 1 ? "" : "s"}`;
6065
+ function spokenMediaTime(seconds) {
6066
+ const total = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
6067
+ const hours = Math.floor(total / 3600);
6068
+ const minutes = Math.floor(total % 3600 / 60);
6069
+ const rest = total % 60;
6070
+ return [
6071
+ hours > 0 ? unit(hours, "hour") : "",
6072
+ minutes > 0 ? unit(minutes, "minute") : "",
6073
+ rest > 0 || total === 0 ? unit(rest, "second") : ""
6074
+ ].filter(Boolean).join(" ");
6075
+ }
6076
+ var frameStripStyles = tailwindVariants.tv({
6077
+ slots: {
6078
+ root: "flex shrink-0 gap-2 overflow-x-auto overscroll-x-contain scroll-px-[18px] px-[18px] py-3",
6079
+ item: [
6080
+ "group flex w-[120px] shrink-0 cursor-pointer flex-col gap-1.5 rounded-md p-1 outline-none",
6081
+ "transition-[background-color,box-shadow] duration-150",
6082
+ "hovered:bg-surface-2",
6083
+ "pressed:bg-surface-3",
6084
+ "data-[focus-visible]:shadow-[var(--shadow-focus)]"
6085
+ ],
6086
+ thumbnail: [
6087
+ "relative aspect-video w-full overflow-hidden rounded-sm border border-rule bg-surface-2",
6088
+ "*:size-full *:object-cover",
6089
+ "transition-[border-color,box-shadow] duration-150",
6090
+ "group-data-[hovered]:border-rule-strong",
6091
+ "group-data-[selected]:border-ink group-data-[selected]:shadow-[inset_0_0_0_1px_var(--color-ink)]"
6092
+ ],
6093
+ caption: "flex min-w-0 items-baseline gap-1.5 px-0.5",
6094
+ label: [
6095
+ "min-w-0 flex-1 truncate text-[12px] leading-4 text-ink-2",
6096
+ "group-data-[selected]:font-medium group-data-[selected]:text-ink"
6097
+ ],
6098
+ time: "shrink-0 font-mono text-[11px] leading-4 tabular-nums text-ink-4"
6099
+ }
6100
+ });
6101
+ function FrameStrip({ items, selectedKey, onSelect, className, ...props }) {
6102
+ const styles4 = frameStripStyles();
6103
+ const rootRef = react.useRef(null);
6104
+ react.useEffect(() => {
6105
+ const root = rootRef.current;
6106
+ if (selectedKey === void 0 || !root) return;
6107
+ for (const element of root.querySelectorAll("[data-frame-key]")) {
6108
+ if (element.dataset.frameKey !== selectedKey) continue;
6109
+ if (typeof element.scrollIntoView === "function")
6110
+ element.scrollIntoView({ block: "nearest", inline: "nearest" });
6111
+ break;
6112
+ }
6113
+ }, [selectedKey]);
6114
+ if (items.length === 0) return null;
6115
+ const isLabelled = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
6116
+ return /* @__PURE__ */ jsxRuntime.jsx(
6117
+ reactAriaComponents.ToggleButtonGroup,
6118
+ {
6119
+ ...isLabelled ? {} : { "aria-label": "Frames" },
6120
+ ...props,
6121
+ ref: rootRef,
6122
+ orientation: "horizontal",
6123
+ selectionMode: "single",
6124
+ disallowEmptySelection: true,
6125
+ selectedKeys: selectedKey === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([selectedKey]),
6126
+ onSelectionChange: (keys) => {
6127
+ const [next] = Array.from(keys);
6128
+ if (next !== void 0) onSelect(String(next));
6129
+ },
6130
+ className: chunk4ACELYEP_cjs.cn(styles4.root(), className),
6131
+ children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
6132
+ reactAriaComponents.ToggleButton,
6133
+ {
6134
+ id: item.key,
6135
+ "data-frame-key": item.key,
6136
+ "aria-label": `${item.label}, at ${formatMediaTime(item.at)}`,
6137
+ className: styles4.item(),
6138
+ children: [
6139
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.thumbnail(), children: item.thumbnail }),
6140
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles4.caption(), children: [
6141
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.label(), children: item.label }),
6142
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.time(), children: formatMediaTime(item.at) })
6143
+ ] })
6144
+ ]
6145
+ },
6146
+ item.key
6147
+ ))
6148
+ }
6149
+ );
6150
+ }
6032
6151
  var glowFrameStyles = tailwindVariants.tv({
6033
6152
  slots: {
6034
6153
  room: "glow-room flex min-h-0 flex-col",
@@ -6233,6 +6352,847 @@ function Input({
6233
6352
  }
6234
6353
  );
6235
6354
  }
6355
+ var sliderStyles = tailwindVariants.tv({
6356
+ slots: {
6357
+ root: "flex w-full items-center",
6358
+ track: "group relative h-[18px] w-full cursor-pointer data-[disabled]:cursor-default",
6359
+ rail: "absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 overflow-hidden rounded-pill bg-surface-3",
6360
+ fill: "h-full bg-ink",
6361
+ thumb: [
6362
+ "top-1/2 size-3 rounded-pill bg-ink",
6363
+ "shadow-[var(--shadow-rest)]",
6364
+ "data-[focus-visible]:shadow-[var(--shadow-focus)]",
6365
+ "data-[disabled]:hidden"
6366
+ ]
6367
+ }
6368
+ });
6369
+ function Slider({
6370
+ value,
6371
+ onChange,
6372
+ onChangeEnd,
6373
+ valueText,
6374
+ className,
6375
+ ...props
6376
+ }) {
6377
+ const styles4 = sliderStyles();
6378
+ const inputRef = react.useRef(null);
6379
+ react.useLayoutEffect(() => {
6380
+ if (valueText !== void 0) inputRef.current?.setAttribute("aria-valuetext", valueText);
6381
+ });
6382
+ return /* @__PURE__ */ jsxRuntime.jsx(
6383
+ reactAriaComponents.Slider,
6384
+ {
6385
+ ...props,
6386
+ value,
6387
+ ...onChange ? { onChange } : {},
6388
+ ...onChangeEnd ? { onChangeEnd } : {},
6389
+ className: (renderProps) => chunk4ACELYEP_cjs.cn(styles4.root(), typeof className === "function" ? className(renderProps) : className),
6390
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactAriaComponents.SliderTrack, { className: styles4.track(), children: ({ state }) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6391
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles4.rail(), children: /* @__PURE__ */ jsxRuntime.jsx(
6392
+ "div",
6393
+ {
6394
+ className: styles4.fill(),
6395
+ style: { width: `${state.getThumbPercent(0) * 100}%` }
6396
+ }
6397
+ ) }),
6398
+ /* @__PURE__ */ jsxRuntime.jsx(reactAriaComponents.SliderThumb, { inputRef, className: styles4.thumb() })
6399
+ ] }) })
6400
+ }
6401
+ );
6402
+ }
6403
+
6404
+ // src/components/media-player/media-peaks.ts
6405
+ var MAX_DECODE_BYTES = 50 * 1024 * 1024;
6406
+ var DECODE_SAMPLE_RATE = 8e3;
6407
+ var PEAK_BUCKETS = 4e3;
6408
+ var SAMPLES_PER_BUCKET = 256;
6409
+ function peaksFromChannels(channels, buckets = PEAK_BUCKETS) {
6410
+ const length = channels[0]?.length ?? 0;
6411
+ const peaks = new Float32Array(buckets);
6412
+ if (length === 0) return peaks;
6413
+ const stride = Math.max(1, Math.floor(length / buckets / SAMPLES_PER_BUCKET));
6414
+ let loudest = 0;
6415
+ for (let bucket = 0; bucket < buckets; bucket++) {
6416
+ const start = Math.floor(bucket / buckets * length);
6417
+ const end = Math.floor((bucket + 1) / buckets * length);
6418
+ let sum = 0;
6419
+ let count = 0;
6420
+ for (let index = start; index < end; index += stride) {
6421
+ let sample = 0;
6422
+ for (const channel of channels) sample = Math.max(sample, Math.abs(channel[index] ?? 0));
6423
+ sum += sample * sample;
6424
+ count++;
6425
+ }
6426
+ const level = count > 0 ? Math.sqrt(sum / count) : 0;
6427
+ peaks[bucket] = level;
6428
+ if (level > loudest) loudest = level;
6429
+ }
6430
+ if (loudest > 0)
6431
+ for (let bucket = 0; bucket < buckets; bucket++) peaks[bucket] = (peaks[bucket] ?? 0) / loudest;
6432
+ return peaks;
6433
+ }
6434
+ function maskPeaks(peaks, duration, segments) {
6435
+ const masked = new Float32Array(peaks.length);
6436
+ if (duration <= 0) return masked;
6437
+ for (const [start, end] of segments) {
6438
+ const from = Math.max(0, Math.floor(start / duration * peaks.length));
6439
+ const to = Math.min(peaks.length, Math.ceil(end / duration * peaks.length));
6440
+ for (let bucket = from; bucket < to; bucket++) masked[bucket] = peaks[bucket] ?? 0;
6441
+ }
6442
+ return masked;
6443
+ }
6444
+ async function decodePeaks(blob) {
6445
+ const context = new OfflineAudioContext(1, 1, DECODE_SAMPLE_RATE);
6446
+ const buffer = await context.decodeAudioData(await blob.arrayBuffer());
6447
+ const channels = Array.from(
6448
+ { length: buffer.numberOfChannels },
6449
+ (_, index) => buffer.getChannelData(index)
6450
+ );
6451
+ return { peaks: peaksFromChannels(channels), duration: buffer.duration };
6452
+ }
6453
+ var mediaPlayerStyles = tailwindVariants.tv({
6454
+ slots: {
6455
+ root: "flex min-h-0 flex-1 flex-col",
6456
+ stage: "relative flex min-h-0 flex-1 flex-col"
6457
+ }
6458
+ });
6459
+ var MediaPlayerContext = react.createContext(null);
6460
+ function useMediaPlayer() {
6461
+ const context = react.useContext(MediaPlayerContext);
6462
+ if (!context) throw new Error("useMediaPlayer must be used inside a MediaPlayer");
6463
+ return context;
6464
+ }
6465
+ var HAVE_METADATA = 1;
6466
+ function MediaPlayer({
6467
+ source,
6468
+ label,
6469
+ kind = "audio",
6470
+ onError,
6471
+ className,
6472
+ children
6473
+ }) {
6474
+ const [media, setMedia] = react.useState(null);
6475
+ const [src, setSrc] = react.useState(null);
6476
+ const [loaded, setLoaded] = react.useState(null);
6477
+ const [isPlaying, setIsPlaying] = react.useState(false);
6478
+ const [currentTime, setCurrentTime] = react.useState(0);
6479
+ const [mediaDuration, setMediaDuration] = react.useState(0);
6480
+ const [playbackRate, setPlaybackRateState] = react.useState(1);
6481
+ const [waveformStatus, setWaveformStatus] = react.useState("decoding");
6482
+ const [decoded, setDecoded] = react.useState(null);
6483
+ const blob = "blob" in source ? source.blob : null;
6484
+ const url = "url" in source ? source.url : null;
6485
+ const givenPeaks = "url" in source ? source.peaks ?? null : null;
6486
+ const givenDuration = source.duration ?? null;
6487
+ react.useEffect(() => {
6488
+ if (!blob) {
6489
+ setSrc(url);
6490
+ return;
6491
+ }
6492
+ const objectUrl = URL.createObjectURL(blob);
6493
+ setSrc(objectUrl);
6494
+ return () => URL.revokeObjectURL(objectUrl);
6495
+ }, [blob, url]);
6496
+ react.useEffect(() => {
6497
+ if (!blob) {
6498
+ if (givenPeaks && givenDuration !== null) {
6499
+ setDecoded({ peaks: givenPeaks, duration: givenDuration });
6500
+ setWaveformStatus("ready");
6501
+ } else {
6502
+ setDecoded(null);
6503
+ setWaveformStatus("unavailable");
6504
+ }
6505
+ return;
6506
+ }
6507
+ setDecoded(null);
6508
+ if (blob.size > MAX_DECODE_BYTES) {
6509
+ setWaveformStatus("unavailable");
6510
+ return;
6511
+ }
6512
+ setWaveformStatus("decoding");
6513
+ let isCancelled = false;
6514
+ decodePeaks(blob).then((result) => {
6515
+ if (isCancelled) return;
6516
+ setDecoded(result);
6517
+ setWaveformStatus("ready");
6518
+ }).catch(() => {
6519
+ if (!isCancelled) setWaveformStatus("unavailable");
6520
+ });
6521
+ return () => {
6522
+ isCancelled = true;
6523
+ };
6524
+ }, [blob, givenPeaks, givenDuration]);
6525
+ react.useEffect(() => {
6526
+ if (!media) return;
6527
+ const syncTime = () => setCurrentTime(media.currentTime);
6528
+ const syncDuration = () => setMediaDuration(Number.isFinite(media.duration) ? media.duration : 0);
6529
+ const syncLoaded = () => {
6530
+ setLoaded(media);
6531
+ syncDuration();
6532
+ };
6533
+ const syncEmptied = () => {
6534
+ setLoaded(null);
6535
+ setMediaDuration(0);
6536
+ setCurrentTime(0);
6537
+ };
6538
+ const syncPlaying = () => setIsPlaying(!media.paused && !media.ended);
6539
+ const handleError = () => onError?.();
6540
+ if (media.readyState >= HAVE_METADATA) syncLoaded();
6541
+ const events = [
6542
+ ["timeupdate", syncTime],
6543
+ ["seeked", syncTime],
6544
+ ["loadedmetadata", syncLoaded],
6545
+ ["durationchange", syncDuration],
6546
+ ["emptied", syncEmptied],
6547
+ ["play", syncPlaying],
6548
+ ["pause", syncPlaying],
6549
+ ["ended", syncPlaying],
6550
+ ["error", handleError]
6551
+ ];
6552
+ for (const [name, handler] of events) media.addEventListener(name, handler);
6553
+ return () => {
6554
+ for (const [name, handler] of events) media.removeEventListener(name, handler);
6555
+ media.pause();
6556
+ };
6557
+ }, [media, onError]);
6558
+ const duration = mediaDuration || givenDuration || decoded?.duration || 0;
6559
+ const isReady = media !== null && loaded === media;
6560
+ const toggle = react.useCallback(() => {
6561
+ if (!media) return;
6562
+ if (media.paused || media.ended) {
6563
+ void media.play().catch(() => void 0);
6564
+ } else {
6565
+ media.pause();
6566
+ }
6567
+ }, [media]);
6568
+ const seek = react.useCallback(
6569
+ (seconds) => {
6570
+ if (!media) return;
6571
+ const limit = Number.isFinite(media.duration) ? media.duration : duration > 0 ? duration : seconds;
6572
+ const next = Math.max(0, Math.min(limit, seconds));
6573
+ media.currentTime = next;
6574
+ setCurrentTime(next);
6575
+ },
6576
+ [media, duration]
6577
+ );
6578
+ const skip = react.useCallback(
6579
+ (seconds) => {
6580
+ if (media) seek(media.currentTime + seconds);
6581
+ },
6582
+ [media, seek]
6583
+ );
6584
+ const stop = react.useCallback(() => {
6585
+ media?.pause();
6586
+ seek(0);
6587
+ }, [media, seek]);
6588
+ const setPlaybackRate = react.useCallback(
6589
+ (rate) => {
6590
+ if (media) media.playbackRate = rate;
6591
+ setPlaybackRateState(rate);
6592
+ },
6593
+ [media]
6594
+ );
6595
+ const value = react.useMemo(
6596
+ () => ({
6597
+ media,
6598
+ mediaRef: setMedia,
6599
+ src,
6600
+ isReady,
6601
+ isPlaying,
6602
+ currentTime,
6603
+ duration,
6604
+ playbackRate,
6605
+ waveformStatus,
6606
+ decoded,
6607
+ toggle,
6608
+ stop,
6609
+ seek,
6610
+ skip,
6611
+ setPlaybackRate
6612
+ }),
6613
+ [
6614
+ media,
6615
+ src,
6616
+ isReady,
6617
+ isPlaying,
6618
+ currentTime,
6619
+ duration,
6620
+ playbackRate,
6621
+ waveformStatus,
6622
+ decoded,
6623
+ toggle,
6624
+ stop,
6625
+ seek,
6626
+ skip,
6627
+ setPlaybackRate
6628
+ ]
6629
+ );
6630
+ const handleKeyDown = (event) => {
6631
+ if (event.code !== "Space") return;
6632
+ if (event.target.closest("button, input")) return;
6633
+ event.preventDefault();
6634
+ toggle();
6635
+ };
6636
+ return /* @__PURE__ */ jsxRuntime.jsx(MediaPlayerContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsxs(
6637
+ "section",
6638
+ {
6639
+ "aria-label": label,
6640
+ onKeyDown: handleKeyDown,
6641
+ className: chunk4ACELYEP_cjs.cn(mediaPlayerStyles().root(), className),
6642
+ children: [
6643
+ kind === "audio" ? /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: setMedia, preload: "metadata", ...src ? { src } : {} }) : null,
6644
+ children
6645
+ ]
6646
+ }
6647
+ ) });
6648
+ }
6649
+ function MediaStage({
6650
+ ref,
6651
+ className,
6652
+ children
6653
+ }) {
6654
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: chunk4ACELYEP_cjs.cn(mediaPlayerStyles().stage(), className), children });
6655
+ }
6656
+ var mediaControlsStyles = tailwindVariants.tv({
6657
+ slots: {
6658
+ root: "shrink-0 border-t border-rule-2 px-[18px] pt-3 pb-3.5",
6659
+ seek: "grid grid-cols-[auto_1fr_auto] items-center gap-2.5",
6660
+ elapsed: "font-mono text-[12.5px] leading-none tabular-nums text-ink-2",
6661
+ total: "font-mono text-[12.5px] leading-none tabular-nums text-ink-4",
6662
+ transport: "mt-2 grid grid-cols-[1fr_auto_1fr] items-center",
6663
+ center: "flex items-center gap-1.5",
6664
+ end: "justify-self-end",
6665
+ text: "font-mono"
6666
+ }
6667
+ });
6668
+ var SKIP_SECONDS = 10;
6669
+ var SPEEDS = [1, 1.25, 1.5, 2];
6670
+ function MediaControls({ describeTime, hasSeek = true, className }) {
6671
+ const {
6672
+ isReady,
6673
+ isPlaying,
6674
+ currentTime,
6675
+ duration,
6676
+ playbackRate,
6677
+ toggle,
6678
+ stop,
6679
+ seek,
6680
+ skip,
6681
+ setPlaybackRate
6682
+ } = useMediaPlayer();
6683
+ const styles4 = mediaControlsStyles();
6684
+ const description = describeTime?.(currentTime);
6685
+ const valueText = `${spokenMediaTime(currentTime)} of ${spokenMediaTime(duration)}${description ? `, ${description}` : ""}`;
6686
+ const nextSpeed = SPEEDS[(SPEEDS.indexOf(playbackRate) + 1) % SPEEDS.length] ?? 1;
6687
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunk4ACELYEP_cjs.cn(styles4.root(), className), children: [
6688
+ hasSeek ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles4.seek(), children: [
6689
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.elapsed(), children: formatMediaTime(currentTime) }),
6690
+ /* @__PURE__ */ jsxRuntime.jsx(
6691
+ Slider,
6692
+ {
6693
+ "aria-label": "Seek",
6694
+ value: currentTime,
6695
+ minValue: 0,
6696
+ maxValue: duration || 1,
6697
+ step: 1,
6698
+ isDisabled: !isReady,
6699
+ valueText,
6700
+ onChange: seek
6701
+ }
6702
+ ),
6703
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.total(), children: duration > 0 ? formatMediaTime(duration) : "--:--" })
6704
+ ] }) : null,
6705
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunk4ACELYEP_cjs.cn(styles4.transport(), !hasSeek && "mt-0"), children: [
6706
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(
6707
+ chunk4ACELYEP_cjs.Button,
6708
+ {
6709
+ variant: "ghost",
6710
+ size: "icon-md",
6711
+ "aria-label": "Stop",
6712
+ isDisabled: !isReady,
6713
+ onPress: stop,
6714
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, {})
6715
+ }
6716
+ ) }),
6717
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles4.center(), children: [
6718
+ /* @__PURE__ */ jsxRuntime.jsxs(
6719
+ chunk4ACELYEP_cjs.Button,
6720
+ {
6721
+ variant: "ghost",
6722
+ size: "sm",
6723
+ "aria-label": `Back ${SKIP_SECONDS} seconds`,
6724
+ isDisabled: !isReady,
6725
+ onPress: () => skip(-SKIP_SECONDS),
6726
+ className: styles4.text(),
6727
+ children: [
6728
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, {}),
6729
+ SKIP_SECONDS
6730
+ ]
6731
+ }
6732
+ ),
6733
+ /* @__PURE__ */ jsxRuntime.jsx(
6734
+ chunk4ACELYEP_cjs.Button,
6735
+ {
6736
+ variant: "cta",
6737
+ size: "icon-lg",
6738
+ "aria-label": isPlaying ? "Pause" : "Play",
6739
+ isDisabled: !isReady,
6740
+ onPress: toggle,
6741
+ children: isPlaying ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pause, { fill: "currentColor" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { fill: "currentColor" })
6742
+ }
6743
+ ),
6744
+ /* @__PURE__ */ jsxRuntime.jsxs(
6745
+ chunk4ACELYEP_cjs.Button,
6746
+ {
6747
+ variant: "ghost",
6748
+ size: "sm",
6749
+ "aria-label": `Forward ${SKIP_SECONDS} seconds`,
6750
+ isDisabled: !isReady,
6751
+ onPress: () => skip(SKIP_SECONDS),
6752
+ className: styles4.text(),
6753
+ children: [
6754
+ SKIP_SECONDS,
6755
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCw, {})
6756
+ ]
6757
+ }
6758
+ )
6759
+ ] }),
6760
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles4.end(), children: /* @__PURE__ */ jsxRuntime.jsxs(
6761
+ chunk4ACELYEP_cjs.Button,
6762
+ {
6763
+ variant: "ghost",
6764
+ size: "sm",
6765
+ "aria-label": `Playback speed ${playbackRate} times`,
6766
+ isDisabled: !isReady,
6767
+ onPress: () => setPlaybackRate(nextSpeed),
6768
+ className: styles4.text(),
6769
+ children: [
6770
+ playbackRate,
6771
+ "\xD7"
6772
+ ]
6773
+ }
6774
+ ) })
6775
+ ] })
6776
+ ] });
6777
+ }
6778
+ var mediaMarkersStyles = tailwindVariants.tv({
6779
+ slots: {
6780
+ root: "flex shrink-0 flex-col gap-[3px]",
6781
+ heading: "flex min-w-0 shrink-0 items-center gap-1.5 text-[12px] leading-4 text-ink-3",
6782
+ dot: "size-[5px] shrink-0 rounded-pill",
6783
+ current: "min-w-0 truncate font-medium text-ink",
6784
+ count: "ml-auto shrink-0 font-mono text-[11px] text-ink-4",
6785
+ track: "relative h-3",
6786
+ marker: [
6787
+ "absolute top-0.5 h-2 min-w-1.5 cursor-pointer rounded-sm border p-0 outline-none",
6788
+ "border-[color-mix(in_oklab,var(--color-accent)_45%,var(--color-rule))]",
6789
+ "bg-[color-mix(in_oklab,var(--color-accent)_14%,var(--color-surface))]",
6790
+ "transition-colors duration-150",
6791
+ "hovered:border-accent hovered:bg-accent",
6792
+ "data-[current]:border-accent data-[current]:bg-accent",
6793
+ "data-[focus-visible]:shadow-[var(--shadow-focus)]",
6794
+ // The bar is thin; the press target isn't.
6795
+ "after:absolute after:inset-x-0 after:-inset-y-2 after:content-['']"
6796
+ ]
6797
+ }
6798
+ });
6799
+ function currentMediaMarkers(markers, time) {
6800
+ return markers.filter((marker) => time >= marker.start && time < marker.end);
6801
+ }
6802
+ function MediaMarkerTrack({
6803
+ markers,
6804
+ label = "Markers",
6805
+ currentTime,
6806
+ duration,
6807
+ onSeek,
6808
+ className
6809
+ }) {
6810
+ const styles4 = mediaMarkersStyles();
6811
+ const current = currentMediaMarkers(markers, currentTime);
6812
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunk4ACELYEP_cjs.cn(styles4.root(), className), children: [
6813
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles4.heading(), children: [
6814
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: styles4.dot() }),
6815
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0", children: label }),
6816
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.current(), children: current.map((marker) => marker.label).join(" \xB7 ") }),
6817
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.count(), children: markers.length })
6818
+ ] }),
6819
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles4.track(), children: markers.map((marker) => /* @__PURE__ */ jsxRuntime.jsx(
6820
+ reactAriaComponents.Button,
6821
+ {
6822
+ "data-media-marker": marker.key,
6823
+ "data-current": current.includes(marker) || void 0,
6824
+ "aria-label": `${marker.label}, at ${formatMediaTime(marker.start)}`,
6825
+ onPress: () => onSeek(marker.start),
6826
+ className: styles4.marker(),
6827
+ style: duration > 0 ? {
6828
+ left: `${marker.start / duration * 100}%`,
6829
+ width: `${(marker.end - marker.start) / duration * 100}%`
6830
+ } : { display: "none" }
6831
+ },
6832
+ marker.key
6833
+ )) })
6834
+ ] });
6835
+ }
6836
+ var mediaTimelineStyles = tailwindVariants.tv({
6837
+ slots: {
6838
+ root: "flex shrink-0 flex-col gap-1 px-[18px] pt-2 pb-3",
6839
+ markers: "pt-1"
6840
+ }
6841
+ });
6842
+ function MediaTimeline({
6843
+ markers = [],
6844
+ markersLabel = "Markers",
6845
+ label = "Timeline",
6846
+ durationOverride,
6847
+ className
6848
+ }) {
6849
+ const { currentTime, duration: playerDuration, isReady, seek } = useMediaPlayer();
6850
+ const duration = durationOverride ?? playerDuration;
6851
+ const max = duration > 0 ? duration : 1;
6852
+ const styles4 = mediaTimelineStyles();
6853
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunk4ACELYEP_cjs.cn(styles4.root(), className), children: [
6854
+ /* @__PURE__ */ jsxRuntime.jsx(
6855
+ Slider,
6856
+ {
6857
+ "aria-label": label,
6858
+ value: Math.min(currentTime, max),
6859
+ minValue: 0,
6860
+ maxValue: max,
6861
+ step: 1,
6862
+ isDisabled: !isReady,
6863
+ valueText: `${spokenMediaTime(currentTime)} of ${spokenMediaTime(duration)}`,
6864
+ onChange: seek
6865
+ }
6866
+ ),
6867
+ markers.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
6868
+ MediaMarkerTrack,
6869
+ {
6870
+ markers,
6871
+ label: markersLabel,
6872
+ currentTime,
6873
+ duration,
6874
+ onSeek: seek,
6875
+ className: styles4.markers()
6876
+ }
6877
+ ) : null
6878
+ ] });
6879
+ }
6880
+ var mediaVideoStageStyles = tailwindVariants.tv({
6881
+ slots: {
6882
+ root: "items-center justify-center overflow-hidden",
6883
+ box: "relative shrink-0 bg-ink",
6884
+ video: "block size-full",
6885
+ overlay: "pointer-events-none absolute inset-0"
6886
+ }
6887
+ });
6888
+ var DEFAULT_ASPECT = 16 / 9;
6889
+ function fitMediaBox(width, height, aspect) {
6890
+ if (width <= 0 || aspect <= 0) return { width: 0, height: 0 };
6891
+ if (height <= 0 || width / height <= aspect) return { width, height: width / aspect };
6892
+ return { width: height * aspect, height };
6893
+ }
6894
+ function MediaVideoStage({ children, className }) {
6895
+ const { src, mediaRef } = useMediaPlayer();
6896
+ const styles4 = mediaVideoStageStyles();
6897
+ const stageRef = react.useRef(null);
6898
+ const [video, setVideo] = react.useState(null);
6899
+ const [aspect, setAspect] = react.useState(DEFAULT_ASPECT);
6900
+ const [box, setBox] = react.useState({ width: 0, height: 0 });
6901
+ const registerVideo = react.useCallback(
6902
+ (element) => {
6903
+ setVideo(element);
6904
+ mediaRef(element);
6905
+ },
6906
+ [mediaRef]
6907
+ );
6908
+ react.useEffect(() => {
6909
+ if (!video) return;
6910
+ const sync2 = () => {
6911
+ if (video.videoWidth > 0 && video.videoHeight > 0)
6912
+ setAspect(video.videoWidth / video.videoHeight);
6913
+ };
6914
+ sync2();
6915
+ video.addEventListener("loadedmetadata", sync2);
6916
+ video.addEventListener("resize", sync2);
6917
+ return () => {
6918
+ video.removeEventListener("loadedmetadata", sync2);
6919
+ video.removeEventListener("resize", sync2);
6920
+ };
6921
+ }, [video]);
6922
+ react.useEffect(() => {
6923
+ const stage = stageRef.current;
6924
+ if (!stage) return;
6925
+ const observer = new ResizeObserver((entries) => {
6926
+ const rect = entries[0]?.contentRect;
6927
+ if (!rect) return;
6928
+ const next = fitMediaBox(rect.width, rect.height, aspect);
6929
+ setBox(
6930
+ (previous) => previous.width === next.width && previous.height === next.height ? previous : next
6931
+ );
6932
+ });
6933
+ observer.observe(stage);
6934
+ return () => observer.disconnect();
6935
+ }, [aspect]);
6936
+ return /* @__PURE__ */ jsxRuntime.jsx(MediaStage, { ref: stageRef, className: chunk4ACELYEP_cjs.cn(styles4.root(), className), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles4.box(), style: { width: box.width, height: box.height }, children: [
6937
+ /* @__PURE__ */ jsxRuntime.jsx(
6938
+ "video",
6939
+ {
6940
+ ref: registerVideo,
6941
+ playsInline: true,
6942
+ preload: "metadata",
6943
+ className: styles4.video(),
6944
+ ...src ? { src } : {}
6945
+ }
6946
+ ),
6947
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles4.overlay(), children })
6948
+ ] }) });
6949
+ }
6950
+ var mediaWaveformStyles = tailwindVariants.tv({
6951
+ slots: {
6952
+ root: "relative flex min-h-0 flex-1 cursor-pointer touch-none flex-col justify-center gap-2.5 px-[18px] py-3.5",
6953
+ lane: "flex min-h-[34px] max-h-24 flex-1 basis-0 flex-col gap-0.5",
6954
+ laneName: "flex min-w-0 shrink-0 items-center gap-1.5 text-[12px] leading-4 text-ink-3",
6955
+ laneDot: "size-[5px] shrink-0 rounded-pill",
6956
+ laneMeta: "ml-auto shrink-0 font-mono text-[11px] text-ink-4",
6957
+ bars: [
6958
+ "relative min-h-0 flex-1",
6959
+ "before:absolute before:inset-x-0 before:top-1/2 before:h-px before:bg-rule-2 before:content-['']"
6960
+ ],
6961
+ canvas: "absolute inset-0",
6962
+ playhead: "pointer-events-none absolute inset-y-0 z-10 w-px bg-ink",
6963
+ hover: "pointer-events-none absolute inset-y-0 z-10 w-px bg-ink-4",
6964
+ chip: [
6965
+ "pointer-events-none absolute top-2 z-20 -translate-x-1/2 whitespace-nowrap rounded-sm bg-ink px-1.5 py-1",
6966
+ "font-mono text-[11px] leading-none text-surface"
6967
+ ],
6968
+ status: "pointer-events-none px-[18px] pb-3 text-[12px] text-ink-3",
6969
+ markers: "border-t border-rule-2 pt-2"
6970
+ },
6971
+ variants: {
6972
+ isActive: {
6973
+ true: { laneName: "font-medium text-ink", laneDot: "bg-ink" }
6974
+ },
6975
+ isSeekable: {
6976
+ false: { root: "cursor-default" }
6977
+ }
6978
+ }
6979
+ });
6980
+ var WHOLE_RECORDING = [{ key: "all", label: null, segments: null }];
6981
+ var isWithin = (segments, time) => segments?.some(([start, end]) => time >= start && time < end) ?? false;
6982
+ function useWaveColors() {
6983
+ const [colors, setColors] = react.useState(() => readColors());
6984
+ react.useEffect(() => {
6985
+ const observer = new MutationObserver(() => setColors(readColors()));
6986
+ observer.observe(document.documentElement, {
6987
+ attributes: true,
6988
+ attributeFilter: ["data-theme"]
6989
+ });
6990
+ return () => observer.disconnect();
6991
+ }, []);
6992
+ return colors;
6993
+ }
6994
+ function readColors() {
6995
+ const style = getComputedStyle(document.documentElement);
6996
+ return {
6997
+ wave: style.getPropertyValue("--color-rule-strong").trim() || "gray",
6998
+ progress: style.getPropertyValue("--color-ink").trim() || "black"
6999
+ };
7000
+ }
7001
+ var LaneBars = react.memo(function LaneBars2({
7002
+ media,
7003
+ peaks,
7004
+ duration,
7005
+ wave,
7006
+ progress,
7007
+ onDrawFailed
7008
+ }) {
7009
+ const containerRef = react.useRef(null);
7010
+ const instanceRef = react.useRef(null);
7011
+ const colorsRef = react.useRef({ wave, progress });
7012
+ colorsRef.current = { wave, progress };
7013
+ react.useEffect(() => {
7014
+ const container = containerRef.current;
7015
+ if (!container) return;
7016
+ let isCancelled = false;
7017
+ let instance = null;
7018
+ let observer = null;
7019
+ import('wavesurfer.js').then(({ default: WaveSurferClass }) => {
7020
+ if (isCancelled) return;
7021
+ instance = WaveSurferClass.create({
7022
+ container,
7023
+ media,
7024
+ peaks: [peaks],
7025
+ duration,
7026
+ height: container.clientHeight || 1,
7027
+ barWidth: 2,
7028
+ barGap: 1,
7029
+ barRadius: 1,
7030
+ cursorWidth: 0,
7031
+ interact: false,
7032
+ normalize: false,
7033
+ autoScroll: false,
7034
+ hideScrollbar: true,
7035
+ // A new instance otherwise resets the shared element to its own default.
7036
+ audioRate: media.playbackRate,
7037
+ waveColor: colorsRef.current.wave,
7038
+ progressColor: colorsRef.current.progress
7039
+ });
7040
+ instanceRef.current = instance;
7041
+ let lastHeight = container.clientHeight;
7042
+ observer = new ResizeObserver(() => {
7043
+ const height = container.clientHeight;
7044
+ if (height > 0 && height !== lastHeight) {
7045
+ lastHeight = height;
7046
+ instance?.setOptions({ height });
7047
+ }
7048
+ });
7049
+ observer.observe(container);
7050
+ }).catch(() => {
7051
+ if (!isCancelled) onDrawFailed();
7052
+ });
7053
+ return () => {
7054
+ isCancelled = true;
7055
+ observer?.disconnect();
7056
+ instance?.destroy();
7057
+ instanceRef.current = null;
7058
+ };
7059
+ }, [media, peaks, duration, onDrawFailed]);
7060
+ react.useEffect(() => {
7061
+ instanceRef.current?.setOptions({ waveColor: wave, progressColor: progress });
7062
+ }, [wave, progress]);
7063
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: mediaWaveformStyles().canvas(), "aria-hidden": true });
7064
+ });
7065
+ function MediaWaveform({
7066
+ lanes: lanesProp,
7067
+ markers = [],
7068
+ markersLabel = "Markers",
7069
+ className
7070
+ }) {
7071
+ const { media, decoded, duration, currentTime, waveformStatus, isReady, isPlaying, seek } = useMediaPlayer();
7072
+ const lanes = lanesProp && lanesProp.length > 0 ? lanesProp : WHOLE_RECORDING;
7073
+ const colors = useWaveColors();
7074
+ const styles4 = mediaWaveformStyles({ isSeekable: isReady });
7075
+ const rootRef = react.useRef(null);
7076
+ const playheadRef = react.useRef(null);
7077
+ const [hover, setHover] = react.useState(null);
7078
+ const [isDrawFailed, setIsDrawFailed] = react.useState(false);
7079
+ const handleDrawFailed = react.useCallback(() => setIsDrawFailed(true), []);
7080
+ const lanePeaks = react.useMemo(
7081
+ () => lanes.map(
7082
+ (lane) => decoded && lane.segments ? maskPeaks(decoded.peaks, decoded.duration, lane.segments) : decoded?.peaks ?? null
7083
+ ),
7084
+ [lanes, decoded]
7085
+ );
7086
+ react.useEffect(() => {
7087
+ const playhead = playheadRef.current;
7088
+ if (!media || !playhead) return;
7089
+ let frame = 0;
7090
+ const place = () => {
7091
+ const position = duration > 0 ? Math.min(1, media.currentTime / duration) : 0;
7092
+ playhead.style.left = `calc(18px + (100% - 36px) * ${position})`;
7093
+ playhead.style.visibility = position > 0 ? "visible" : "hidden";
7094
+ };
7095
+ const loop = () => {
7096
+ place();
7097
+ frame = requestAnimationFrame(loop);
7098
+ };
7099
+ place();
7100
+ if (isPlaying) frame = requestAnimationFrame(loop);
7101
+ media.addEventListener("seeked", place);
7102
+ return () => {
7103
+ cancelAnimationFrame(frame);
7104
+ media.removeEventListener("seeked", place);
7105
+ };
7106
+ }, [media, duration, isPlaying]);
7107
+ const timeAt = (clientX) => {
7108
+ const rect = rootRef.current?.getBoundingClientRect();
7109
+ if (!rect || duration <= 0) return 0;
7110
+ return (Math.max(0, Math.min(1, (clientX - rect.left - 18) / (rect.width - 36))) || 0) * duration;
7111
+ };
7112
+ const handlePointerDown = (event) => {
7113
+ if (!isReady || event.target.closest("[data-media-marker]")) return;
7114
+ event.currentTarget.setPointerCapture(event.pointerId);
7115
+ seek(timeAt(event.clientX));
7116
+ };
7117
+ const handlePointerMove = (event) => {
7118
+ if (!isReady) return;
7119
+ const time = timeAt(event.clientX);
7120
+ if (event.buttons > 0 && event.currentTarget.hasPointerCapture(event.pointerId)) seek(time);
7121
+ const target = event.target;
7122
+ const markerKey = target.closest("[data-media-marker]")?.dataset.mediaMarker;
7123
+ const laneKey = target.closest("[data-media-lane]")?.dataset.mediaLane;
7124
+ const marker = markers.find((entry) => entry.key === markerKey);
7125
+ const lane = lanes.find((entry) => entry.key === laneKey);
7126
+ const name = marker?.label ?? (lane?.label && isWithin(lane.segments, time) ? lane.label : null);
7127
+ const rect = event.currentTarget.getBoundingClientRect();
7128
+ setHover({
7129
+ x: event.clientX - rect.left,
7130
+ text: name ? `${name} \xB7 ${formatMediaTime(time)}` : formatMediaTime(time)
7131
+ });
7132
+ };
7133
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7134
+ /* @__PURE__ */ jsxRuntime.jsxs(
7135
+ "div",
7136
+ {
7137
+ ref: rootRef,
7138
+ onPointerDown: handlePointerDown,
7139
+ onPointerMove: handlePointerMove,
7140
+ onPointerLeave: () => setHover(null),
7141
+ className: chunk4ACELYEP_cjs.cn(styles4.root(), className),
7142
+ children: [
7143
+ lanes.map((lane, index) => {
7144
+ const isActive = lane.segments !== null && isWithin(lane.segments, currentTime);
7145
+ const peaks = lanePeaks[index];
7146
+ const laneStyles = mediaWaveformStyles({ isActive });
7147
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-media-lane": lane.key, className: styles4.lane(), children: [
7148
+ lane.label !== null ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: laneStyles.laneName(), "data-active": isActive || void 0, children: [
7149
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: laneStyles.laneDot() }),
7150
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: lane.label }),
7151
+ lane.meta ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles4.laneMeta(), children: lane.meta }) : null
7152
+ ] }) : null,
7153
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles4.bars(), children: media && peaks && decoded ? /* @__PURE__ */ jsxRuntime.jsx(
7154
+ LaneBars,
7155
+ {
7156
+ media,
7157
+ peaks,
7158
+ duration: decoded.duration,
7159
+ wave: colors.wave,
7160
+ progress: colors.progress,
7161
+ onDrawFailed: handleDrawFailed
7162
+ }
7163
+ ) : null })
7164
+ ] }, lane.key);
7165
+ }),
7166
+ markers.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
7167
+ MediaMarkerTrack,
7168
+ {
7169
+ markers,
7170
+ label: markersLabel,
7171
+ currentTime,
7172
+ duration,
7173
+ onSeek: seek,
7174
+ className: styles4.markers()
7175
+ }
7176
+ ) : null,
7177
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: playheadRef, className: styles4.playhead() }),
7178
+ hover ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7179
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles4.hover(), style: { left: hover.x } }),
7180
+ /* @__PURE__ */ jsxRuntime.jsx(
7181
+ "span",
7182
+ {
7183
+ className: styles4.chip(),
7184
+ style: { left: `clamp(70px, ${hover.x}px, calc(100% - 70px))` },
7185
+ children: hover.text
7186
+ }
7187
+ )
7188
+ ] }) : null
7189
+ ]
7190
+ }
7191
+ ),
7192
+ waveformStatus === "decoding" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: styles4.status(), children: "Drawing waveform" }) : null,
7193
+ waveformStatus === "unavailable" || isDrawFailed ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: styles4.status(), children: "Waveform isn\u2019t available. Use the slider to seek." }) : null
7194
+ ] });
7195
+ }
6236
7196
  var MenuTrigger = reactAriaComponents.MenuTrigger;
6237
7197
  var SubmenuTrigger = reactAriaComponents.SubmenuTrigger;
6238
7198
  function MenuPopover({ className, offset = 4, placement = "bottom start", ...props }) {
@@ -9466,6 +10426,7 @@ exports.FileViewer = FileViewer;
9466
10426
  exports.FileViewerFallbackCard = FileViewerFallbackCard;
9467
10427
  exports.FilterRow = FilterRow;
9468
10428
  exports.FilterValueInput = FilterValueInput;
10429
+ exports.FrameStrip = FrameStrip;
9469
10430
  exports.GlowFrame = GlowFrame;
9470
10431
  exports.IconPicker = IconPicker;
9471
10432
  exports.ImageAttachment = ImageAttachment;
@@ -9474,6 +10435,14 @@ exports.Input = Input;
9474
10435
  exports.Kbd = Kbd;
9475
10436
  exports.LinkCell = LinkCell;
9476
10437
  exports.MAX_COMPOSER_ATTACHMENT_BYTES = MAX_COMPOSER_ATTACHMENT_BYTES;
10438
+ exports.MAX_DECODE_BYTES = MAX_DECODE_BYTES;
10439
+ exports.MediaControls = MediaControls;
10440
+ exports.MediaMarkerTrack = MediaMarkerTrack;
10441
+ exports.MediaPlayer = MediaPlayer;
10442
+ exports.MediaStage = MediaStage;
10443
+ exports.MediaTimeline = MediaTimeline;
10444
+ exports.MediaVideoStage = MediaVideoStage;
10445
+ exports.MediaWaveform = MediaWaveform;
9477
10446
  exports.Menu = Menu;
9478
10447
  exports.MenuItem = MenuItem;
9479
10448
  exports.MenuPopover = MenuPopover;
@@ -9521,6 +10490,7 @@ exports.SELECTION_COLUMN_ID = SELECTION_COLUMN_ID;
9521
10490
  exports.SegmentedControl = SegmentedControl;
9522
10491
  exports.Select = Select;
9523
10492
  exports.Separator = Separator;
10493
+ exports.Slider = Slider;
9524
10494
  exports.Stepper = Stepper;
9525
10495
  exports.StepperStep = StepperStep;
9526
10496
  exports.SubmenuTrigger = SubmenuTrigger;
@@ -9566,6 +10536,8 @@ exports.countLeaves = countLeaves;
9566
10536
  exports.createConversationBriefClient = createConversationBriefClient;
9567
10537
  exports.createDraftMessageClient = createDraftMessageClient;
9568
10538
  exports.createToastQueue = createToastQueue;
10539
+ exports.currentMediaMarkers = currentMediaMarkers;
10540
+ exports.decodePeaks = decodePeaks;
9569
10541
  exports.defaultValueFor = defaultValueFor;
9570
10542
  exports.draftMessageCardStyles = draftMessageCardStyles;
9571
10543
  exports.draftMessageComposerStyles = draftMessageComposerStyles;
@@ -9578,8 +10550,11 @@ exports.fieldLabelStyles = fieldLabelStyles;
9578
10550
  exports.fileViewerFallbackCardStyles = fileViewerFallbackCardStyles;
9579
10551
  exports.fileViewerStyles = fileViewerStyles;
9580
10552
  exports.fitBadgeCount = fitBadgeCount;
10553
+ exports.fitMediaBox = fitMediaBox;
9581
10554
  exports.formatFileSize = formatFileSize;
10555
+ exports.formatMediaTime = formatMediaTime;
9582
10556
  exports.formatValue = formatValue;
10557
+ exports.frameStripStyles = frameStripStyles;
9583
10558
  exports.getPlatform = getPlatform;
9584
10559
  exports.glowFrameStyles = glowFrameStyles;
9585
10560
  exports.iconForContentType = iconForContentType;
@@ -9592,6 +10567,13 @@ exports.isEditableTarget = isEditableTarget;
9592
10567
  exports.isPreviewableImageName = isPreviewableImageName;
9593
10568
  exports.joinFieldLabel = joinFieldLabel;
9594
10569
  exports.makeActionsColumn = makeActionsColumn;
10570
+ exports.maskPeaks = maskPeaks;
10571
+ exports.mediaControlsStyles = mediaControlsStyles;
10572
+ exports.mediaMarkersStyles = mediaMarkersStyles;
10573
+ exports.mediaPlayerStyles = mediaPlayerStyles;
10574
+ exports.mediaTimelineStyles = mediaTimelineStyles;
10575
+ exports.mediaVideoStageStyles = mediaVideoStageStyles;
10576
+ exports.mediaWaveformStyles = mediaWaveformStyles;
9595
10577
  exports.messageComposerStyles = messageComposerStyles;
9596
10578
  exports.namesOf = namesOf;
9597
10579
  exports.navItemStyles = navItemStyles;
@@ -9599,6 +10581,7 @@ exports.normalizeHexColor = normalizeHexColor;
9599
10581
  exports.operatorById = operatorById;
9600
10582
  exports.operatorsForType = operatorsForType;
9601
10583
  exports.parseCombo = parseCombo;
10584
+ exports.peaksFromChannels = peaksFromChannels;
9602
10585
  exports.progressBarStyles = progressBarStyles;
9603
10586
  exports.progressRingStyles = progressRingStyles;
9604
10587
  exports.publishDraftMessageOutcome = publishDraftMessageOutcome;
@@ -9614,6 +10597,8 @@ exports.resolveLucideIcon = resolveLucideIcon;
9614
10597
  exports.resolveLucideIconName = resolveLucideIconName;
9615
10598
  exports.segmentedControlStyles = segmentedControlStyles;
9616
10599
  exports.setJoiner = setJoiner;
10600
+ exports.sliderStyles = sliderStyles;
10601
+ exports.spokenMediaTime = spokenMediaTime;
9617
10602
  exports.stepperStyles = stepperStyles;
9618
10603
  exports.subscribeDraftMessageOutcome = subscribeDraftMessageOutcome;
9619
10604
  exports.summaryFor = summaryFor;
@@ -9634,6 +10619,7 @@ exports.useDraftMessageActions = useDraftMessageActions;
9634
10619
  exports.useDraftMessageOutcome = useDraftMessageOutcome;
9635
10620
  exports.useEscapeKey = useEscapeKey;
9636
10621
  exports.useInfiniteScrollSentinel = useInfiniteScrollSentinel;
10622
+ exports.useMediaPlayer = useMediaPlayer;
9637
10623
  exports.useRenderChipItem = useRenderChipItem;
9638
10624
  exports.useRenderValueInput = useRenderValueInput;
9639
10625
  exports.useShowDetails = useShowDetails;