@proteos/ui 0.76.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.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { cn, Button, Skeleton } from './chunk-XRN5MNDX.js';
2
2
  export { Button, Skeleton, buttonStyles, cn, skeletonStyles } from './chunk-XRN5MNDX.js';
3
- import { icons, Contact, Paperclip, BookText, Banknote, UsersRound, User, Link2, Braces, Brackets, ToggleLeft, CircleDot, Calendar, Hash, Type, File, FileImage, FileText, FileSpreadsheet, FileArchive, Loader2, X, ChevronLeft, ChevronRight, Shapes, Search, Phone, AudioLines, Video, Mic, AtSign, MessagesSquare, Camera, Send, Linkedin, MessageCircle, Mail, Pipette, Pencil, ChevronDown, GripVertical, SendHorizontal, CircleX, Download, AlertCircle, Undo2, ArrowRight, Check, CircleCheck, CircleAlert, Info } from 'lucide-react';
3
+ import { icons, Contact, Paperclip, BookText, Banknote, UsersRound, User, Link2, Braces, Brackets, ToggleLeft, CircleDot, Calendar, Hash, Type, File, FileImage, FileText, FileSpreadsheet, FileArchive, Loader2, X, ChevronLeft, ChevronRight, Shapes, Search, Phone, AudioLines, Video, Mic, AtSign, MessagesSquare, Camera, Send, Linkedin, MessageCircle, Mail, Pipette, Pencil, ChevronDown, GripVertical, SendHorizontal, CircleX, Download, Square, RotateCcw, Pause, Play, RotateCw, AlertCircle, Undo2, ArrowRight, Check, CircleCheck, CircleAlert, Info } from 'lucide-react';
4
4
  import { tv } from 'tailwind-variants';
5
- import { Tabs as Tabs$1, TabPanel, Tab, TabList, MenuTrigger as MenuTrigger$1, SubmenuTrigger as SubmenuTrigger$1, DialogTrigger, Popover as Popover$1, Dialog, DropZone, Text, FileTrigger, Heading, Calendar as Calendar$1, Button as Button$1, CalendarGrid, CalendarGridHeader, CalendarHeaderCell, CalendarGridBody, CalendarCell, CalendarStateContext, Label, Input as Input$1, ListBox, ListBoxItem, TooltipTrigger as TooltipTrigger$1, Tooltip as Tooltip$1, Breadcrumbs as Breadcrumbs$1, Breadcrumb, Link, Checkbox as Checkbox$1, CheckboxGroup as CheckboxGroup$1, FieldError, TextField, ComboBox, ComboBoxStateContext, Select as Select$1, SelectValue, ModalOverlay, Modal as Modal$1, DatePicker as DatePicker$1, Group, DateInput, DateSegment, Menu as Menu$1, MenuItem as MenuItem$1, Disclosure, DisclosurePanel, ProgressBar as ProgressBar$1, RadioGroup as RadioGroup$1, Radio as Radio$1, Separator as Separator$1, Switch as Switch$1, TextArea, TimeField as TimeField$1, ToggleButtonGroup, ToggleButton, UNSTABLE_ToastQueue, UNSTABLE_ToastRegion, UNSTABLE_Toast, UNSTABLE_ToastContent, DatePickerStateContext } from 'react-aria-components';
5
+ import { Tabs as Tabs$1, TabPanel, Tab, TabList, MenuTrigger as MenuTrigger$1, SubmenuTrigger as SubmenuTrigger$1, DialogTrigger, Popover as Popover$1, Dialog, DropZone, Text, FileTrigger, Heading, Calendar as Calendar$1, Button as Button$1, CalendarGrid, CalendarGridHeader, CalendarHeaderCell, CalendarGridBody, CalendarCell, CalendarStateContext, Label, Input as Input$1, ListBox, ListBoxItem, TooltipTrigger as TooltipTrigger$1, Tooltip as Tooltip$1, Breadcrumbs as Breadcrumbs$1, Breadcrumb, Link, Checkbox as Checkbox$1, CheckboxGroup as CheckboxGroup$1, FieldError, TextField, ComboBox, ComboBoxStateContext, Select as Select$1, SelectValue, ModalOverlay, Modal as Modal$1, DatePicker as DatePicker$1, Group, DateInput, DateSegment, ToggleButtonGroup, ToggleButton, Slider as Slider$1, SliderTrack, SliderThumb, Menu as Menu$1, MenuItem as MenuItem$1, Disclosure, DisclosurePanel, ProgressBar as ProgressBar$1, RadioGroup as RadioGroup$1, Radio as Radio$1, Separator as Separator$1, Switch as Switch$1, TextArea, TimeField as TimeField$1, UNSTABLE_ToastQueue, UNSTABLE_ToastRegion, UNSTABLE_Toast, UNSTABLE_ToastContent, DatePickerStateContext } from 'react-aria-components';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
- import { lazy, createContext, useContext, useState, useRef, useMemo, Children, isValidElement, useLayoutEffect, useEffect, useSyncExternalStore, useCallback, Suspense, useId, Fragment as Fragment$1 } from 'react';
7
+ import { lazy, createContext, memo, useRef, useEffect, useContext, useState, useMemo, Children, isValidElement, useLayoutEffect, useSyncExternalStore, useCallback, Suspense, useId, Fragment as Fragment$1 } from 'react';
8
8
  import { formatAmount, parseAmount } from '@proteos/sdk';
9
9
  import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
10
10
  import { extractClosestEdge, attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
@@ -125,12 +125,12 @@ function formatFileSize(bytes) {
125
125
  if (bytes < 1024) return `${bytes} B`;
126
126
  const units = ["KB", "MB", "GB", "TB"];
127
127
  let size = bytes / 1024;
128
- let unit = 0;
129
- while (size >= 1024 && unit < units.length - 1) {
128
+ let unit2 = 0;
129
+ while (size >= 1024 && unit2 < units.length - 1) {
130
130
  size /= 1024;
131
- unit += 1;
131
+ unit2 += 1;
132
132
  }
133
- return `${size < 10 ? size.toFixed(1) : Math.round(size)} ${units[unit]}`;
133
+ return `${size < 10 ? size.toFixed(1) : Math.round(size)} ${units[unit2]}`;
134
134
  }
135
135
  function FileField({
136
136
  value,
@@ -3997,13 +3997,35 @@ function DataTable({
3997
3997
  setListOffset((prev) => prev === top ? prev : top);
3998
3998
  });
3999
3999
  const headingHeight = groupHeaderHeight ?? rowHeight;
4000
+ const [isScrollerAttached, setIsScrollerAttached] = useState(false);
4001
+ useLayoutEffect(() => setIsScrollerAttached(true), []);
4000
4002
  const virtualizer = useVirtualizer({
4003
+ enabled: initialScrollOffset === void 0 || isScrollerAttached,
4001
4004
  count: listItems.length,
4002
4005
  getScrollElement: () => scrollerRef.current,
4003
4006
  estimateSize: (index) => listItems[index]?.kind === "group" ? headingHeight : rowHeight,
4004
4007
  overscan,
4005
4008
  scrollMargin: listOffset,
4006
- ...initialScrollOffset !== void 0 ? { initialOffset: initialScrollOffset } : {}
4009
+ // Read once, with the scroller attached (see `enabled`). Clamp the restored
4010
+ // offset to what the scroller will be able to reach: while the skeleton
4011
+ // shows, or when the list shrank since the visit, the browser clamps
4012
+ // scrollTop WITHOUT firing a scroll event, and the virtualizer — which only
4013
+ // re-reads its offset on scroll events — would sit past the rows and paint
4014
+ // empty. The reach comes from the row data, not `scrollHeight`: the disabled
4015
+ // first render committed an empty spacer, so the DOM can't report it yet.
4016
+ ...initialScrollOffset !== void 0 ? {
4017
+ initialOffset: () => {
4018
+ const el = scrollerRef.current;
4019
+ if (!el) return 0;
4020
+ const estimatedSize = listItems.reduce(
4021
+ (sum, item) => sum + (item.kind === "group" ? headingHeight : rowHeight),
4022
+ 0
4023
+ );
4024
+ const listTop = listRef.current?.offsetTop ?? 0;
4025
+ const reachable = listTop + estimatedSize - el.clientHeight;
4026
+ return Math.max(0, Math.min(initialScrollOffset, reachable));
4027
+ }
4028
+ } : {}
4007
4029
  });
4008
4030
  const sentinelRef = useInfiniteScrollSentinel({
4009
4031
  onIntersect: () => {
@@ -6028,6 +6050,103 @@ function FileViewer({
6028
6050
  const skeleton = /* @__PURE__ */ jsx(Skeleton, { "aria-label": `Loading preview of ${name}`, className: "h-full w-full rounded-none" });
6029
6051
  return /* @__PURE__ */ jsx("div", { className: cn(fileViewerStyles(), className), style: { height: heightPx }, children: !src ? skeleton : kind === "image" ? /* @__PURE__ */ jsx(ImageView, { name, src, ...onDownload ? { onDownload } : {} }) : kind === "video" ? /* @__PURE__ */ jsx(VideoView, { name, src }) : /* @__PURE__ */ jsx(Suspense, { fallback: skeleton, children: /* @__PURE__ */ jsx(PdfView, { name, src, ...onDownload ? { onDownload } : {} }) }) });
6030
6052
  }
6053
+
6054
+ // src/components/media-player/media-time.ts
6055
+ var pad = (value) => String(value).padStart(2, "0");
6056
+ function formatMediaTime(seconds) {
6057
+ const total = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
6058
+ const hours = Math.floor(total / 3600);
6059
+ const minutes = Math.floor(total % 3600 / 60);
6060
+ const rest = total % 60;
6061
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(rest)}` : `${pad(minutes)}:${pad(rest)}`;
6062
+ }
6063
+ var unit = (value, name) => `${value} ${name}${value === 1 ? "" : "s"}`;
6064
+ function spokenMediaTime(seconds) {
6065
+ const total = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
6066
+ const hours = Math.floor(total / 3600);
6067
+ const minutes = Math.floor(total % 3600 / 60);
6068
+ const rest = total % 60;
6069
+ return [
6070
+ hours > 0 ? unit(hours, "hour") : "",
6071
+ minutes > 0 ? unit(minutes, "minute") : "",
6072
+ rest > 0 || total === 0 ? unit(rest, "second") : ""
6073
+ ].filter(Boolean).join(" ");
6074
+ }
6075
+ var frameStripStyles = tv({
6076
+ slots: {
6077
+ root: "flex shrink-0 gap-2 overflow-x-auto overscroll-x-contain scroll-px-[18px] px-[18px] py-3",
6078
+ item: [
6079
+ "group flex w-[120px] shrink-0 cursor-pointer flex-col gap-1.5 rounded-md p-1 outline-none",
6080
+ "transition-[background-color,box-shadow] duration-150",
6081
+ "hovered:bg-surface-2",
6082
+ "pressed:bg-surface-3",
6083
+ "data-[focus-visible]:shadow-[var(--shadow-focus)]"
6084
+ ],
6085
+ thumbnail: [
6086
+ "relative aspect-video w-full overflow-hidden rounded-sm border border-rule bg-surface-2",
6087
+ "*:size-full *:object-cover",
6088
+ "transition-[border-color,box-shadow] duration-150",
6089
+ "group-data-[hovered]:border-rule-strong",
6090
+ "group-data-[selected]:border-ink group-data-[selected]:shadow-[inset_0_0_0_1px_var(--color-ink)]"
6091
+ ],
6092
+ caption: "flex min-w-0 items-baseline gap-1.5 px-0.5",
6093
+ label: [
6094
+ "min-w-0 flex-1 truncate text-[12px] leading-4 text-ink-2",
6095
+ "group-data-[selected]:font-medium group-data-[selected]:text-ink"
6096
+ ],
6097
+ time: "shrink-0 font-mono text-[11px] leading-4 tabular-nums text-ink-4"
6098
+ }
6099
+ });
6100
+ function FrameStrip({ items, selectedKey, onSelect, className, ...props }) {
6101
+ const styles4 = frameStripStyles();
6102
+ const rootRef = useRef(null);
6103
+ useEffect(() => {
6104
+ const root = rootRef.current;
6105
+ if (selectedKey === void 0 || !root) return;
6106
+ for (const element of root.querySelectorAll("[data-frame-key]")) {
6107
+ if (element.dataset.frameKey !== selectedKey) continue;
6108
+ if (typeof element.scrollIntoView === "function")
6109
+ element.scrollIntoView({ block: "nearest", inline: "nearest" });
6110
+ break;
6111
+ }
6112
+ }, [selectedKey]);
6113
+ if (items.length === 0) return null;
6114
+ const isLabelled = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
6115
+ return /* @__PURE__ */ jsx(
6116
+ ToggleButtonGroup,
6117
+ {
6118
+ ...isLabelled ? {} : { "aria-label": "Frames" },
6119
+ ...props,
6120
+ ref: rootRef,
6121
+ orientation: "horizontal",
6122
+ selectionMode: "single",
6123
+ disallowEmptySelection: true,
6124
+ selectedKeys: selectedKey === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([selectedKey]),
6125
+ onSelectionChange: (keys) => {
6126
+ const [next] = Array.from(keys);
6127
+ if (next !== void 0) onSelect(String(next));
6128
+ },
6129
+ className: cn(styles4.root(), className),
6130
+ children: items.map((item) => /* @__PURE__ */ jsxs(
6131
+ ToggleButton,
6132
+ {
6133
+ id: item.key,
6134
+ "data-frame-key": item.key,
6135
+ "aria-label": `${item.label}, at ${formatMediaTime(item.at)}`,
6136
+ className: styles4.item(),
6137
+ children: [
6138
+ /* @__PURE__ */ jsx("span", { className: styles4.thumbnail(), children: item.thumbnail }),
6139
+ /* @__PURE__ */ jsxs("span", { className: styles4.caption(), children: [
6140
+ /* @__PURE__ */ jsx("span", { className: styles4.label(), children: item.label }),
6141
+ /* @__PURE__ */ jsx("span", { className: styles4.time(), children: formatMediaTime(item.at) })
6142
+ ] })
6143
+ ]
6144
+ },
6145
+ item.key
6146
+ ))
6147
+ }
6148
+ );
6149
+ }
6031
6150
  var glowFrameStyles = tv({
6032
6151
  slots: {
6033
6152
  room: "glow-room flex min-h-0 flex-col",
@@ -6232,6 +6351,847 @@ function Input({
6232
6351
  }
6233
6352
  );
6234
6353
  }
6354
+ var sliderStyles = tv({
6355
+ slots: {
6356
+ root: "flex w-full items-center",
6357
+ track: "group relative h-[18px] w-full cursor-pointer data-[disabled]:cursor-default",
6358
+ rail: "absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 overflow-hidden rounded-pill bg-surface-3",
6359
+ fill: "h-full bg-ink",
6360
+ thumb: [
6361
+ "top-1/2 size-3 rounded-pill bg-ink",
6362
+ "shadow-[var(--shadow-rest)]",
6363
+ "data-[focus-visible]:shadow-[var(--shadow-focus)]",
6364
+ "data-[disabled]:hidden"
6365
+ ]
6366
+ }
6367
+ });
6368
+ function Slider({
6369
+ value,
6370
+ onChange,
6371
+ onChangeEnd,
6372
+ valueText,
6373
+ className,
6374
+ ...props
6375
+ }) {
6376
+ const styles4 = sliderStyles();
6377
+ const inputRef = useRef(null);
6378
+ useLayoutEffect(() => {
6379
+ if (valueText !== void 0) inputRef.current?.setAttribute("aria-valuetext", valueText);
6380
+ });
6381
+ return /* @__PURE__ */ jsx(
6382
+ Slider$1,
6383
+ {
6384
+ ...props,
6385
+ value,
6386
+ ...onChange ? { onChange } : {},
6387
+ ...onChangeEnd ? { onChangeEnd } : {},
6388
+ className: (renderProps) => cn(styles4.root(), typeof className === "function" ? className(renderProps) : className),
6389
+ children: /* @__PURE__ */ jsx(SliderTrack, { className: styles4.track(), children: ({ state }) => /* @__PURE__ */ jsxs(Fragment, { children: [
6390
+ /* @__PURE__ */ jsx("div", { className: styles4.rail(), children: /* @__PURE__ */ jsx(
6391
+ "div",
6392
+ {
6393
+ className: styles4.fill(),
6394
+ style: { width: `${state.getThumbPercent(0) * 100}%` }
6395
+ }
6396
+ ) }),
6397
+ /* @__PURE__ */ jsx(SliderThumb, { inputRef, className: styles4.thumb() })
6398
+ ] }) })
6399
+ }
6400
+ );
6401
+ }
6402
+
6403
+ // src/components/media-player/media-peaks.ts
6404
+ var MAX_DECODE_BYTES = 50 * 1024 * 1024;
6405
+ var DECODE_SAMPLE_RATE = 8e3;
6406
+ var PEAK_BUCKETS = 4e3;
6407
+ var SAMPLES_PER_BUCKET = 256;
6408
+ function peaksFromChannels(channels, buckets = PEAK_BUCKETS) {
6409
+ const length = channels[0]?.length ?? 0;
6410
+ const peaks = new Float32Array(buckets);
6411
+ if (length === 0) return peaks;
6412
+ const stride = Math.max(1, Math.floor(length / buckets / SAMPLES_PER_BUCKET));
6413
+ let loudest = 0;
6414
+ for (let bucket = 0; bucket < buckets; bucket++) {
6415
+ const start = Math.floor(bucket / buckets * length);
6416
+ const end = Math.floor((bucket + 1) / buckets * length);
6417
+ let sum = 0;
6418
+ let count = 0;
6419
+ for (let index = start; index < end; index += stride) {
6420
+ let sample = 0;
6421
+ for (const channel of channels) sample = Math.max(sample, Math.abs(channel[index] ?? 0));
6422
+ sum += sample * sample;
6423
+ count++;
6424
+ }
6425
+ const level = count > 0 ? Math.sqrt(sum / count) : 0;
6426
+ peaks[bucket] = level;
6427
+ if (level > loudest) loudest = level;
6428
+ }
6429
+ if (loudest > 0)
6430
+ for (let bucket = 0; bucket < buckets; bucket++) peaks[bucket] = (peaks[bucket] ?? 0) / loudest;
6431
+ return peaks;
6432
+ }
6433
+ function maskPeaks(peaks, duration, segments) {
6434
+ const masked = new Float32Array(peaks.length);
6435
+ if (duration <= 0) return masked;
6436
+ for (const [start, end] of segments) {
6437
+ const from = Math.max(0, Math.floor(start / duration * peaks.length));
6438
+ const to = Math.min(peaks.length, Math.ceil(end / duration * peaks.length));
6439
+ for (let bucket = from; bucket < to; bucket++) masked[bucket] = peaks[bucket] ?? 0;
6440
+ }
6441
+ return masked;
6442
+ }
6443
+ async function decodePeaks(blob) {
6444
+ const context = new OfflineAudioContext(1, 1, DECODE_SAMPLE_RATE);
6445
+ const buffer = await context.decodeAudioData(await blob.arrayBuffer());
6446
+ const channels = Array.from(
6447
+ { length: buffer.numberOfChannels },
6448
+ (_, index) => buffer.getChannelData(index)
6449
+ );
6450
+ return { peaks: peaksFromChannels(channels), duration: buffer.duration };
6451
+ }
6452
+ var mediaPlayerStyles = tv({
6453
+ slots: {
6454
+ root: "flex min-h-0 flex-1 flex-col",
6455
+ stage: "relative flex min-h-0 flex-1 flex-col"
6456
+ }
6457
+ });
6458
+ var MediaPlayerContext = createContext(null);
6459
+ function useMediaPlayer() {
6460
+ const context = useContext(MediaPlayerContext);
6461
+ if (!context) throw new Error("useMediaPlayer must be used inside a MediaPlayer");
6462
+ return context;
6463
+ }
6464
+ var HAVE_METADATA = 1;
6465
+ function MediaPlayer({
6466
+ source,
6467
+ label,
6468
+ kind = "audio",
6469
+ onError,
6470
+ className,
6471
+ children
6472
+ }) {
6473
+ const [media, setMedia] = useState(null);
6474
+ const [src, setSrc] = useState(null);
6475
+ const [loaded, setLoaded] = useState(null);
6476
+ const [isPlaying, setIsPlaying] = useState(false);
6477
+ const [currentTime, setCurrentTime] = useState(0);
6478
+ const [mediaDuration, setMediaDuration] = useState(0);
6479
+ const [playbackRate, setPlaybackRateState] = useState(1);
6480
+ const [waveformStatus, setWaveformStatus] = useState("decoding");
6481
+ const [decoded, setDecoded] = useState(null);
6482
+ const blob = "blob" in source ? source.blob : null;
6483
+ const url = "url" in source ? source.url : null;
6484
+ const givenPeaks = "url" in source ? source.peaks ?? null : null;
6485
+ const givenDuration = source.duration ?? null;
6486
+ useEffect(() => {
6487
+ if (!blob) {
6488
+ setSrc(url);
6489
+ return;
6490
+ }
6491
+ const objectUrl = URL.createObjectURL(blob);
6492
+ setSrc(objectUrl);
6493
+ return () => URL.revokeObjectURL(objectUrl);
6494
+ }, [blob, url]);
6495
+ useEffect(() => {
6496
+ if (!blob) {
6497
+ if (givenPeaks && givenDuration !== null) {
6498
+ setDecoded({ peaks: givenPeaks, duration: givenDuration });
6499
+ setWaveformStatus("ready");
6500
+ } else {
6501
+ setDecoded(null);
6502
+ setWaveformStatus("unavailable");
6503
+ }
6504
+ return;
6505
+ }
6506
+ setDecoded(null);
6507
+ if (blob.size > MAX_DECODE_BYTES) {
6508
+ setWaveformStatus("unavailable");
6509
+ return;
6510
+ }
6511
+ setWaveformStatus("decoding");
6512
+ let isCancelled = false;
6513
+ decodePeaks(blob).then((result) => {
6514
+ if (isCancelled) return;
6515
+ setDecoded(result);
6516
+ setWaveformStatus("ready");
6517
+ }).catch(() => {
6518
+ if (!isCancelled) setWaveformStatus("unavailable");
6519
+ });
6520
+ return () => {
6521
+ isCancelled = true;
6522
+ };
6523
+ }, [blob, givenPeaks, givenDuration]);
6524
+ useEffect(() => {
6525
+ if (!media) return;
6526
+ const syncTime = () => setCurrentTime(media.currentTime);
6527
+ const syncDuration = () => setMediaDuration(Number.isFinite(media.duration) ? media.duration : 0);
6528
+ const syncLoaded = () => {
6529
+ setLoaded(media);
6530
+ syncDuration();
6531
+ };
6532
+ const syncEmptied = () => {
6533
+ setLoaded(null);
6534
+ setMediaDuration(0);
6535
+ setCurrentTime(0);
6536
+ };
6537
+ const syncPlaying = () => setIsPlaying(!media.paused && !media.ended);
6538
+ const handleError = () => onError?.();
6539
+ if (media.readyState >= HAVE_METADATA) syncLoaded();
6540
+ const events = [
6541
+ ["timeupdate", syncTime],
6542
+ ["seeked", syncTime],
6543
+ ["loadedmetadata", syncLoaded],
6544
+ ["durationchange", syncDuration],
6545
+ ["emptied", syncEmptied],
6546
+ ["play", syncPlaying],
6547
+ ["pause", syncPlaying],
6548
+ ["ended", syncPlaying],
6549
+ ["error", handleError]
6550
+ ];
6551
+ for (const [name, handler] of events) media.addEventListener(name, handler);
6552
+ return () => {
6553
+ for (const [name, handler] of events) media.removeEventListener(name, handler);
6554
+ media.pause();
6555
+ };
6556
+ }, [media, onError]);
6557
+ const duration = mediaDuration || givenDuration || decoded?.duration || 0;
6558
+ const isReady = media !== null && loaded === media;
6559
+ const toggle = useCallback(() => {
6560
+ if (!media) return;
6561
+ if (media.paused || media.ended) {
6562
+ void media.play().catch(() => void 0);
6563
+ } else {
6564
+ media.pause();
6565
+ }
6566
+ }, [media]);
6567
+ const seek = useCallback(
6568
+ (seconds) => {
6569
+ if (!media) return;
6570
+ const limit = Number.isFinite(media.duration) ? media.duration : duration > 0 ? duration : seconds;
6571
+ const next = Math.max(0, Math.min(limit, seconds));
6572
+ media.currentTime = next;
6573
+ setCurrentTime(next);
6574
+ },
6575
+ [media, duration]
6576
+ );
6577
+ const skip = useCallback(
6578
+ (seconds) => {
6579
+ if (media) seek(media.currentTime + seconds);
6580
+ },
6581
+ [media, seek]
6582
+ );
6583
+ const stop = useCallback(() => {
6584
+ media?.pause();
6585
+ seek(0);
6586
+ }, [media, seek]);
6587
+ const setPlaybackRate = useCallback(
6588
+ (rate) => {
6589
+ if (media) media.playbackRate = rate;
6590
+ setPlaybackRateState(rate);
6591
+ },
6592
+ [media]
6593
+ );
6594
+ const value = useMemo(
6595
+ () => ({
6596
+ media,
6597
+ mediaRef: setMedia,
6598
+ src,
6599
+ isReady,
6600
+ isPlaying,
6601
+ currentTime,
6602
+ duration,
6603
+ playbackRate,
6604
+ waveformStatus,
6605
+ decoded,
6606
+ toggle,
6607
+ stop,
6608
+ seek,
6609
+ skip,
6610
+ setPlaybackRate
6611
+ }),
6612
+ [
6613
+ media,
6614
+ src,
6615
+ isReady,
6616
+ isPlaying,
6617
+ currentTime,
6618
+ duration,
6619
+ playbackRate,
6620
+ waveformStatus,
6621
+ decoded,
6622
+ toggle,
6623
+ stop,
6624
+ seek,
6625
+ skip,
6626
+ setPlaybackRate
6627
+ ]
6628
+ );
6629
+ const handleKeyDown = (event) => {
6630
+ if (event.code !== "Space") return;
6631
+ if (event.target.closest("button, input")) return;
6632
+ event.preventDefault();
6633
+ toggle();
6634
+ };
6635
+ return /* @__PURE__ */ jsx(MediaPlayerContext.Provider, { value, children: /* @__PURE__ */ jsxs(
6636
+ "section",
6637
+ {
6638
+ "aria-label": label,
6639
+ onKeyDown: handleKeyDown,
6640
+ className: cn(mediaPlayerStyles().root(), className),
6641
+ children: [
6642
+ kind === "audio" ? /* @__PURE__ */ jsx("audio", { ref: setMedia, preload: "metadata", ...src ? { src } : {} }) : null,
6643
+ children
6644
+ ]
6645
+ }
6646
+ ) });
6647
+ }
6648
+ function MediaStage({
6649
+ ref,
6650
+ className,
6651
+ children
6652
+ }) {
6653
+ return /* @__PURE__ */ jsx("div", { ref, className: cn(mediaPlayerStyles().stage(), className), children });
6654
+ }
6655
+ var mediaControlsStyles = tv({
6656
+ slots: {
6657
+ root: "shrink-0 border-t border-rule-2 px-[18px] pt-3 pb-3.5",
6658
+ seek: "grid grid-cols-[auto_1fr_auto] items-center gap-2.5",
6659
+ elapsed: "font-mono text-[12.5px] leading-none tabular-nums text-ink-2",
6660
+ total: "font-mono text-[12.5px] leading-none tabular-nums text-ink-4",
6661
+ transport: "mt-2 grid grid-cols-[1fr_auto_1fr] items-center",
6662
+ center: "flex items-center gap-1.5",
6663
+ end: "justify-self-end",
6664
+ text: "font-mono"
6665
+ }
6666
+ });
6667
+ var SKIP_SECONDS = 10;
6668
+ var SPEEDS = [1, 1.25, 1.5, 2];
6669
+ function MediaControls({ describeTime, hasSeek = true, className }) {
6670
+ const {
6671
+ isReady,
6672
+ isPlaying,
6673
+ currentTime,
6674
+ duration,
6675
+ playbackRate,
6676
+ toggle,
6677
+ stop,
6678
+ seek,
6679
+ skip,
6680
+ setPlaybackRate
6681
+ } = useMediaPlayer();
6682
+ const styles4 = mediaControlsStyles();
6683
+ const description = describeTime?.(currentTime);
6684
+ const valueText = `${spokenMediaTime(currentTime)} of ${spokenMediaTime(duration)}${description ? `, ${description}` : ""}`;
6685
+ const nextSpeed = SPEEDS[(SPEEDS.indexOf(playbackRate) + 1) % SPEEDS.length] ?? 1;
6686
+ return /* @__PURE__ */ jsxs("div", { className: cn(styles4.root(), className), children: [
6687
+ hasSeek ? /* @__PURE__ */ jsxs("div", { className: styles4.seek(), children: [
6688
+ /* @__PURE__ */ jsx("span", { className: styles4.elapsed(), children: formatMediaTime(currentTime) }),
6689
+ /* @__PURE__ */ jsx(
6690
+ Slider,
6691
+ {
6692
+ "aria-label": "Seek",
6693
+ value: currentTime,
6694
+ minValue: 0,
6695
+ maxValue: duration || 1,
6696
+ step: 1,
6697
+ isDisabled: !isReady,
6698
+ valueText,
6699
+ onChange: seek
6700
+ }
6701
+ ),
6702
+ /* @__PURE__ */ jsx("span", { className: styles4.total(), children: duration > 0 ? formatMediaTime(duration) : "--:--" })
6703
+ ] }) : null,
6704
+ /* @__PURE__ */ jsxs("div", { className: cn(styles4.transport(), !hasSeek && "mt-0"), children: [
6705
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
6706
+ Button,
6707
+ {
6708
+ variant: "ghost",
6709
+ size: "icon-md",
6710
+ "aria-label": "Stop",
6711
+ isDisabled: !isReady,
6712
+ onPress: stop,
6713
+ children: /* @__PURE__ */ jsx(Square, {})
6714
+ }
6715
+ ) }),
6716
+ /* @__PURE__ */ jsxs("div", { className: styles4.center(), children: [
6717
+ /* @__PURE__ */ jsxs(
6718
+ Button,
6719
+ {
6720
+ variant: "ghost",
6721
+ size: "sm",
6722
+ "aria-label": `Back ${SKIP_SECONDS} seconds`,
6723
+ isDisabled: !isReady,
6724
+ onPress: () => skip(-SKIP_SECONDS),
6725
+ className: styles4.text(),
6726
+ children: [
6727
+ /* @__PURE__ */ jsx(RotateCcw, {}),
6728
+ SKIP_SECONDS
6729
+ ]
6730
+ }
6731
+ ),
6732
+ /* @__PURE__ */ jsx(
6733
+ Button,
6734
+ {
6735
+ variant: "cta",
6736
+ size: "icon-lg",
6737
+ "aria-label": isPlaying ? "Pause" : "Play",
6738
+ isDisabled: !isReady,
6739
+ onPress: toggle,
6740
+ children: isPlaying ? /* @__PURE__ */ jsx(Pause, { fill: "currentColor" }) : /* @__PURE__ */ jsx(Play, { fill: "currentColor" })
6741
+ }
6742
+ ),
6743
+ /* @__PURE__ */ jsxs(
6744
+ Button,
6745
+ {
6746
+ variant: "ghost",
6747
+ size: "sm",
6748
+ "aria-label": `Forward ${SKIP_SECONDS} seconds`,
6749
+ isDisabled: !isReady,
6750
+ onPress: () => skip(SKIP_SECONDS),
6751
+ className: styles4.text(),
6752
+ children: [
6753
+ SKIP_SECONDS,
6754
+ /* @__PURE__ */ jsx(RotateCw, {})
6755
+ ]
6756
+ }
6757
+ )
6758
+ ] }),
6759
+ /* @__PURE__ */ jsx("div", { className: styles4.end(), children: /* @__PURE__ */ jsxs(
6760
+ Button,
6761
+ {
6762
+ variant: "ghost",
6763
+ size: "sm",
6764
+ "aria-label": `Playback speed ${playbackRate} times`,
6765
+ isDisabled: !isReady,
6766
+ onPress: () => setPlaybackRate(nextSpeed),
6767
+ className: styles4.text(),
6768
+ children: [
6769
+ playbackRate,
6770
+ "\xD7"
6771
+ ]
6772
+ }
6773
+ ) })
6774
+ ] })
6775
+ ] });
6776
+ }
6777
+ var mediaMarkersStyles = tv({
6778
+ slots: {
6779
+ root: "flex shrink-0 flex-col gap-[3px]",
6780
+ heading: "flex min-w-0 shrink-0 items-center gap-1.5 text-[12px] leading-4 text-ink-3",
6781
+ dot: "size-[5px] shrink-0 rounded-pill",
6782
+ current: "min-w-0 truncate font-medium text-ink",
6783
+ count: "ml-auto shrink-0 font-mono text-[11px] text-ink-4",
6784
+ track: "relative h-3",
6785
+ marker: [
6786
+ "absolute top-0.5 h-2 min-w-1.5 cursor-pointer rounded-sm border p-0 outline-none",
6787
+ "border-[color-mix(in_oklab,var(--color-accent)_45%,var(--color-rule))]",
6788
+ "bg-[color-mix(in_oklab,var(--color-accent)_14%,var(--color-surface))]",
6789
+ "transition-colors duration-150",
6790
+ "hovered:border-accent hovered:bg-accent",
6791
+ "data-[current]:border-accent data-[current]:bg-accent",
6792
+ "data-[focus-visible]:shadow-[var(--shadow-focus)]",
6793
+ // The bar is thin; the press target isn't.
6794
+ "after:absolute after:inset-x-0 after:-inset-y-2 after:content-['']"
6795
+ ]
6796
+ }
6797
+ });
6798
+ function currentMediaMarkers(markers, time) {
6799
+ return markers.filter((marker) => time >= marker.start && time < marker.end);
6800
+ }
6801
+ function MediaMarkerTrack({
6802
+ markers,
6803
+ label = "Markers",
6804
+ currentTime,
6805
+ duration,
6806
+ onSeek,
6807
+ className
6808
+ }) {
6809
+ const styles4 = mediaMarkersStyles();
6810
+ const current = currentMediaMarkers(markers, currentTime);
6811
+ return /* @__PURE__ */ jsxs("div", { className: cn(styles4.root(), className), children: [
6812
+ /* @__PURE__ */ jsxs("div", { className: styles4.heading(), children: [
6813
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: styles4.dot() }),
6814
+ /* @__PURE__ */ jsx("span", { className: "shrink-0", children: label }),
6815
+ /* @__PURE__ */ jsx("span", { className: styles4.current(), children: current.map((marker) => marker.label).join(" \xB7 ") }),
6816
+ /* @__PURE__ */ jsx("span", { className: styles4.count(), children: markers.length })
6817
+ ] }),
6818
+ /* @__PURE__ */ jsx("div", { className: styles4.track(), children: markers.map((marker) => /* @__PURE__ */ jsx(
6819
+ Button$1,
6820
+ {
6821
+ "data-media-marker": marker.key,
6822
+ "data-current": current.includes(marker) || void 0,
6823
+ "aria-label": `${marker.label}, at ${formatMediaTime(marker.start)}`,
6824
+ onPress: () => onSeek(marker.start),
6825
+ className: styles4.marker(),
6826
+ style: duration > 0 ? {
6827
+ left: `${marker.start / duration * 100}%`,
6828
+ width: `${(marker.end - marker.start) / duration * 100}%`
6829
+ } : { display: "none" }
6830
+ },
6831
+ marker.key
6832
+ )) })
6833
+ ] });
6834
+ }
6835
+ var mediaTimelineStyles = tv({
6836
+ slots: {
6837
+ root: "flex shrink-0 flex-col gap-1 px-[18px] pt-2 pb-3",
6838
+ markers: "pt-1"
6839
+ }
6840
+ });
6841
+ function MediaTimeline({
6842
+ markers = [],
6843
+ markersLabel = "Markers",
6844
+ label = "Timeline",
6845
+ durationOverride,
6846
+ className
6847
+ }) {
6848
+ const { currentTime, duration: playerDuration, isReady, seek } = useMediaPlayer();
6849
+ const duration = durationOverride ?? playerDuration;
6850
+ const max = duration > 0 ? duration : 1;
6851
+ const styles4 = mediaTimelineStyles();
6852
+ return /* @__PURE__ */ jsxs("div", { className: cn(styles4.root(), className), children: [
6853
+ /* @__PURE__ */ jsx(
6854
+ Slider,
6855
+ {
6856
+ "aria-label": label,
6857
+ value: Math.min(currentTime, max),
6858
+ minValue: 0,
6859
+ maxValue: max,
6860
+ step: 1,
6861
+ isDisabled: !isReady,
6862
+ valueText: `${spokenMediaTime(currentTime)} of ${spokenMediaTime(duration)}`,
6863
+ onChange: seek
6864
+ }
6865
+ ),
6866
+ markers.length > 0 ? /* @__PURE__ */ jsx(
6867
+ MediaMarkerTrack,
6868
+ {
6869
+ markers,
6870
+ label: markersLabel,
6871
+ currentTime,
6872
+ duration,
6873
+ onSeek: seek,
6874
+ className: styles4.markers()
6875
+ }
6876
+ ) : null
6877
+ ] });
6878
+ }
6879
+ var mediaVideoStageStyles = tv({
6880
+ slots: {
6881
+ root: "items-center justify-center overflow-hidden",
6882
+ box: "relative shrink-0 bg-ink",
6883
+ video: "block size-full",
6884
+ overlay: "pointer-events-none absolute inset-0"
6885
+ }
6886
+ });
6887
+ var DEFAULT_ASPECT = 16 / 9;
6888
+ function fitMediaBox(width, height, aspect) {
6889
+ if (width <= 0 || aspect <= 0) return { width: 0, height: 0 };
6890
+ if (height <= 0 || width / height <= aspect) return { width, height: width / aspect };
6891
+ return { width: height * aspect, height };
6892
+ }
6893
+ function MediaVideoStage({ children, className }) {
6894
+ const { src, mediaRef } = useMediaPlayer();
6895
+ const styles4 = mediaVideoStageStyles();
6896
+ const stageRef = useRef(null);
6897
+ const [video, setVideo] = useState(null);
6898
+ const [aspect, setAspect] = useState(DEFAULT_ASPECT);
6899
+ const [box, setBox] = useState({ width: 0, height: 0 });
6900
+ const registerVideo = useCallback(
6901
+ (element) => {
6902
+ setVideo(element);
6903
+ mediaRef(element);
6904
+ },
6905
+ [mediaRef]
6906
+ );
6907
+ useEffect(() => {
6908
+ if (!video) return;
6909
+ const sync2 = () => {
6910
+ if (video.videoWidth > 0 && video.videoHeight > 0)
6911
+ setAspect(video.videoWidth / video.videoHeight);
6912
+ };
6913
+ sync2();
6914
+ video.addEventListener("loadedmetadata", sync2);
6915
+ video.addEventListener("resize", sync2);
6916
+ return () => {
6917
+ video.removeEventListener("loadedmetadata", sync2);
6918
+ video.removeEventListener("resize", sync2);
6919
+ };
6920
+ }, [video]);
6921
+ useEffect(() => {
6922
+ const stage = stageRef.current;
6923
+ if (!stage) return;
6924
+ const observer = new ResizeObserver((entries) => {
6925
+ const rect = entries[0]?.contentRect;
6926
+ if (!rect) return;
6927
+ const next = fitMediaBox(rect.width, rect.height, aspect);
6928
+ setBox(
6929
+ (previous) => previous.width === next.width && previous.height === next.height ? previous : next
6930
+ );
6931
+ });
6932
+ observer.observe(stage);
6933
+ return () => observer.disconnect();
6934
+ }, [aspect]);
6935
+ return /* @__PURE__ */ jsx(MediaStage, { ref: stageRef, className: cn(styles4.root(), className), children: /* @__PURE__ */ jsxs("div", { className: styles4.box(), style: { width: box.width, height: box.height }, children: [
6936
+ /* @__PURE__ */ jsx(
6937
+ "video",
6938
+ {
6939
+ ref: registerVideo,
6940
+ playsInline: true,
6941
+ preload: "metadata",
6942
+ className: styles4.video(),
6943
+ ...src ? { src } : {}
6944
+ }
6945
+ ),
6946
+ /* @__PURE__ */ jsx("div", { className: styles4.overlay(), children })
6947
+ ] }) });
6948
+ }
6949
+ var mediaWaveformStyles = tv({
6950
+ slots: {
6951
+ root: "relative flex min-h-0 flex-1 cursor-pointer touch-none flex-col justify-center gap-2.5 px-[18px] py-3.5",
6952
+ lane: "flex min-h-[34px] max-h-24 flex-1 basis-0 flex-col gap-0.5",
6953
+ laneName: "flex min-w-0 shrink-0 items-center gap-1.5 text-[12px] leading-4 text-ink-3",
6954
+ laneDot: "size-[5px] shrink-0 rounded-pill",
6955
+ laneMeta: "ml-auto shrink-0 font-mono text-[11px] text-ink-4",
6956
+ bars: [
6957
+ "relative min-h-0 flex-1",
6958
+ "before:absolute before:inset-x-0 before:top-1/2 before:h-px before:bg-rule-2 before:content-['']"
6959
+ ],
6960
+ canvas: "absolute inset-0",
6961
+ playhead: "pointer-events-none absolute inset-y-0 z-10 w-px bg-ink",
6962
+ hover: "pointer-events-none absolute inset-y-0 z-10 w-px bg-ink-4",
6963
+ chip: [
6964
+ "pointer-events-none absolute top-2 z-20 -translate-x-1/2 whitespace-nowrap rounded-sm bg-ink px-1.5 py-1",
6965
+ "font-mono text-[11px] leading-none text-surface"
6966
+ ],
6967
+ status: "pointer-events-none px-[18px] pb-3 text-[12px] text-ink-3",
6968
+ markers: "border-t border-rule-2 pt-2"
6969
+ },
6970
+ variants: {
6971
+ isActive: {
6972
+ true: { laneName: "font-medium text-ink", laneDot: "bg-ink" }
6973
+ },
6974
+ isSeekable: {
6975
+ false: { root: "cursor-default" }
6976
+ }
6977
+ }
6978
+ });
6979
+ var WHOLE_RECORDING = [{ key: "all", label: null, segments: null }];
6980
+ var isWithin = (segments, time) => segments?.some(([start, end]) => time >= start && time < end) ?? false;
6981
+ function useWaveColors() {
6982
+ const [colors, setColors] = useState(() => readColors());
6983
+ useEffect(() => {
6984
+ const observer = new MutationObserver(() => setColors(readColors()));
6985
+ observer.observe(document.documentElement, {
6986
+ attributes: true,
6987
+ attributeFilter: ["data-theme"]
6988
+ });
6989
+ return () => observer.disconnect();
6990
+ }, []);
6991
+ return colors;
6992
+ }
6993
+ function readColors() {
6994
+ const style = getComputedStyle(document.documentElement);
6995
+ return {
6996
+ wave: style.getPropertyValue("--color-rule-strong").trim() || "gray",
6997
+ progress: style.getPropertyValue("--color-ink").trim() || "black"
6998
+ };
6999
+ }
7000
+ var LaneBars = memo(function LaneBars2({
7001
+ media,
7002
+ peaks,
7003
+ duration,
7004
+ wave,
7005
+ progress,
7006
+ onDrawFailed
7007
+ }) {
7008
+ const containerRef = useRef(null);
7009
+ const instanceRef = useRef(null);
7010
+ const colorsRef = useRef({ wave, progress });
7011
+ colorsRef.current = { wave, progress };
7012
+ useEffect(() => {
7013
+ const container = containerRef.current;
7014
+ if (!container) return;
7015
+ let isCancelled = false;
7016
+ let instance = null;
7017
+ let observer = null;
7018
+ import('wavesurfer.js').then(({ default: WaveSurferClass }) => {
7019
+ if (isCancelled) return;
7020
+ instance = WaveSurferClass.create({
7021
+ container,
7022
+ media,
7023
+ peaks: [peaks],
7024
+ duration,
7025
+ height: container.clientHeight || 1,
7026
+ barWidth: 2,
7027
+ barGap: 1,
7028
+ barRadius: 1,
7029
+ cursorWidth: 0,
7030
+ interact: false,
7031
+ normalize: false,
7032
+ autoScroll: false,
7033
+ hideScrollbar: true,
7034
+ // A new instance otherwise resets the shared element to its own default.
7035
+ audioRate: media.playbackRate,
7036
+ waveColor: colorsRef.current.wave,
7037
+ progressColor: colorsRef.current.progress
7038
+ });
7039
+ instanceRef.current = instance;
7040
+ let lastHeight = container.clientHeight;
7041
+ observer = new ResizeObserver(() => {
7042
+ const height = container.clientHeight;
7043
+ if (height > 0 && height !== lastHeight) {
7044
+ lastHeight = height;
7045
+ instance?.setOptions({ height });
7046
+ }
7047
+ });
7048
+ observer.observe(container);
7049
+ }).catch(() => {
7050
+ if (!isCancelled) onDrawFailed();
7051
+ });
7052
+ return () => {
7053
+ isCancelled = true;
7054
+ observer?.disconnect();
7055
+ instance?.destroy();
7056
+ instanceRef.current = null;
7057
+ };
7058
+ }, [media, peaks, duration, onDrawFailed]);
7059
+ useEffect(() => {
7060
+ instanceRef.current?.setOptions({ waveColor: wave, progressColor: progress });
7061
+ }, [wave, progress]);
7062
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, className: mediaWaveformStyles().canvas(), "aria-hidden": true });
7063
+ });
7064
+ function MediaWaveform({
7065
+ lanes: lanesProp,
7066
+ markers = [],
7067
+ markersLabel = "Markers",
7068
+ className
7069
+ }) {
7070
+ const { media, decoded, duration, currentTime, waveformStatus, isReady, isPlaying, seek } = useMediaPlayer();
7071
+ const lanes = lanesProp && lanesProp.length > 0 ? lanesProp : WHOLE_RECORDING;
7072
+ const colors = useWaveColors();
7073
+ const styles4 = mediaWaveformStyles({ isSeekable: isReady });
7074
+ const rootRef = useRef(null);
7075
+ const playheadRef = useRef(null);
7076
+ const [hover, setHover] = useState(null);
7077
+ const [isDrawFailed, setIsDrawFailed] = useState(false);
7078
+ const handleDrawFailed = useCallback(() => setIsDrawFailed(true), []);
7079
+ const lanePeaks = useMemo(
7080
+ () => lanes.map(
7081
+ (lane) => decoded && lane.segments ? maskPeaks(decoded.peaks, decoded.duration, lane.segments) : decoded?.peaks ?? null
7082
+ ),
7083
+ [lanes, decoded]
7084
+ );
7085
+ useEffect(() => {
7086
+ const playhead = playheadRef.current;
7087
+ if (!media || !playhead) return;
7088
+ let frame = 0;
7089
+ const place = () => {
7090
+ const position = duration > 0 ? Math.min(1, media.currentTime / duration) : 0;
7091
+ playhead.style.left = `calc(18px + (100% - 36px) * ${position})`;
7092
+ playhead.style.visibility = position > 0 ? "visible" : "hidden";
7093
+ };
7094
+ const loop = () => {
7095
+ place();
7096
+ frame = requestAnimationFrame(loop);
7097
+ };
7098
+ place();
7099
+ if (isPlaying) frame = requestAnimationFrame(loop);
7100
+ media.addEventListener("seeked", place);
7101
+ return () => {
7102
+ cancelAnimationFrame(frame);
7103
+ media.removeEventListener("seeked", place);
7104
+ };
7105
+ }, [media, duration, isPlaying]);
7106
+ const timeAt = (clientX) => {
7107
+ const rect = rootRef.current?.getBoundingClientRect();
7108
+ if (!rect || duration <= 0) return 0;
7109
+ return (Math.max(0, Math.min(1, (clientX - rect.left - 18) / (rect.width - 36))) || 0) * duration;
7110
+ };
7111
+ const handlePointerDown = (event) => {
7112
+ if (!isReady || event.target.closest("[data-media-marker]")) return;
7113
+ event.currentTarget.setPointerCapture(event.pointerId);
7114
+ seek(timeAt(event.clientX));
7115
+ };
7116
+ const handlePointerMove = (event) => {
7117
+ if (!isReady) return;
7118
+ const time = timeAt(event.clientX);
7119
+ if (event.buttons > 0 && event.currentTarget.hasPointerCapture(event.pointerId)) seek(time);
7120
+ const target = event.target;
7121
+ const markerKey = target.closest("[data-media-marker]")?.dataset.mediaMarker;
7122
+ const laneKey = target.closest("[data-media-lane]")?.dataset.mediaLane;
7123
+ const marker = markers.find((entry) => entry.key === markerKey);
7124
+ const lane = lanes.find((entry) => entry.key === laneKey);
7125
+ const name = marker?.label ?? (lane?.label && isWithin(lane.segments, time) ? lane.label : null);
7126
+ const rect = event.currentTarget.getBoundingClientRect();
7127
+ setHover({
7128
+ x: event.clientX - rect.left,
7129
+ text: name ? `${name} \xB7 ${formatMediaTime(time)}` : formatMediaTime(time)
7130
+ });
7131
+ };
7132
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7133
+ /* @__PURE__ */ jsxs(
7134
+ "div",
7135
+ {
7136
+ ref: rootRef,
7137
+ onPointerDown: handlePointerDown,
7138
+ onPointerMove: handlePointerMove,
7139
+ onPointerLeave: () => setHover(null),
7140
+ className: cn(styles4.root(), className),
7141
+ children: [
7142
+ lanes.map((lane, index) => {
7143
+ const isActive = lane.segments !== null && isWithin(lane.segments, currentTime);
7144
+ const peaks = lanePeaks[index];
7145
+ const laneStyles = mediaWaveformStyles({ isActive });
7146
+ return /* @__PURE__ */ jsxs("div", { "data-media-lane": lane.key, className: styles4.lane(), children: [
7147
+ lane.label !== null ? /* @__PURE__ */ jsxs("div", { className: laneStyles.laneName(), "data-active": isActive || void 0, children: [
7148
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: laneStyles.laneDot() }),
7149
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: lane.label }),
7150
+ lane.meta ? /* @__PURE__ */ jsx("span", { className: styles4.laneMeta(), children: lane.meta }) : null
7151
+ ] }) : null,
7152
+ /* @__PURE__ */ jsx("div", { className: styles4.bars(), children: media && peaks && decoded ? /* @__PURE__ */ jsx(
7153
+ LaneBars,
7154
+ {
7155
+ media,
7156
+ peaks,
7157
+ duration: decoded.duration,
7158
+ wave: colors.wave,
7159
+ progress: colors.progress,
7160
+ onDrawFailed: handleDrawFailed
7161
+ }
7162
+ ) : null })
7163
+ ] }, lane.key);
7164
+ }),
7165
+ markers.length > 0 ? /* @__PURE__ */ jsx(
7166
+ MediaMarkerTrack,
7167
+ {
7168
+ markers,
7169
+ label: markersLabel,
7170
+ currentTime,
7171
+ duration,
7172
+ onSeek: seek,
7173
+ className: styles4.markers()
7174
+ }
7175
+ ) : null,
7176
+ /* @__PURE__ */ jsx("div", { ref: playheadRef, className: styles4.playhead() }),
7177
+ hover ? /* @__PURE__ */ jsxs(Fragment, { children: [
7178
+ /* @__PURE__ */ jsx("div", { className: styles4.hover(), style: { left: hover.x } }),
7179
+ /* @__PURE__ */ jsx(
7180
+ "span",
7181
+ {
7182
+ className: styles4.chip(),
7183
+ style: { left: `clamp(70px, ${hover.x}px, calc(100% - 70px))` },
7184
+ children: hover.text
7185
+ }
7186
+ )
7187
+ ] }) : null
7188
+ ]
7189
+ }
7190
+ ),
7191
+ waveformStatus === "decoding" ? /* @__PURE__ */ jsx("p", { className: styles4.status(), children: "Drawing waveform" }) : null,
7192
+ waveformStatus === "unavailable" || isDrawFailed ? /* @__PURE__ */ jsx("p", { className: styles4.status(), children: "Waveform isn\u2019t available. Use the slider to seek." }) : null
7193
+ ] });
7194
+ }
6235
7195
  var MenuTrigger = MenuTrigger$1;
6236
7196
  var SubmenuTrigger = SubmenuTrigger$1;
6237
7197
  function MenuPopover({ className, offset = 4, placement = "bottom start", ...props }) {
@@ -9387,6 +10347,6 @@ function ToastRegion({ queue, className }) {
9387
10347
  );
9388
10348
  }
9389
10349
 
9390
- export { ATTRIBUTE_TYPES, ATTRIBUTE_TYPE_ICONS, AttachmentChip, AttributePicker, AttributeType, AttributeTypeIcon, AvailabilityCalendar, Avatar, AvatarGroup, Badge, BadgeAction, BadgeGroup, BadgeMultiSelect, BooleanCell, Breadcrumbs, COLUMN_DRAG_KIND, CONVERSATION_BRIEF_OUTCOME_LABEL, Card, CardBody, CardContent, CardEyebrow, CardFeatureTitle, CardFooter, CardHeader, CardTitle, ChannelIcon, Checkbox, CheckboxGroup, ChipValue, ColorInput, Combobox, Command, CommandHint, ConversationBriefCard, ConversationBriefPreview, ConversationBriefReview, CurrencyInput, DRAFT_OUTCOME_LABEL, DataTable, DataTableEmpty, DataTableError, DataTableSkeleton, DateCell, DatePicker, DateTimeCell, DateTimePicker, DraftMessageCard, DraftMessageComposer, DraftMessageReview, EmptyState, ErrorAnimation, FIELD_LABEL_SEPARATOR, FieldCaret, FileField, FileViewer, FileViewerFallbackCard, FilterRow, FilterValueInput, GlowFrame, IconPicker, ImageAttachment, ImageLightbox, Input, Kbd, LinkCell, MAX_COMPOSER_ATTACHMENT_BYTES, Menu, MenuItem, MenuPopover, MenuTrigger, MessageComposer, Modal, ModalBody, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalTitle, MultiSelect, NavGroup, NavItem, NavSection, OPERATORS, OperatorPicker, Orb, PROTECTED_COMBOS, Popover, PopoverContent, PopoverPanelBody, PopoverPanelFooter, PopoverPanelHeader, PressableCard, ProgressBar, ProgressRing, Radio, RadioGroup, RecipientSelect, RecordFilterBuilderPanel, RecordFilterButton, RecordFilterChips, RecordFilterEditor, RecordUpdate, RecordUpdateErrors, RecordUpdateFooter, RecordUpdateRow, RecordUpdateRows, RecordUpdateSummary, RenderChipItemProvider, RenderValueInputProvider, SELECTION_COLUMN_ID, SegmentedControl, Select, Separator, Stepper, StepperStep, SubmenuTrigger, SuccessAnimation, Switch, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableHeaderSpacer, TableRow, TableScroller, Tabs, TagInput, TextShimmer, Textarea, TimeField, TimeSlotList, ToastRegion, Tooltip, TooltipTrigger, TruncateCell, __resetDraftMessageActions, __resetDraftMessageOutcomes, addLeaf as addFilterLeaf, addPendingLeaf, addSubgroup, arityOf, attachmentChipStyles, availabilityCalendarStyles, avatarGroupStyles, avatarStyles, badgeGroupOverflowStyles, badgeGroupStyles, badgeStyles, cardStyles2 as cardStyles, channelIcon, contactPhoneNumbers, conversationBriefCardStyles, conversationBriefOutcomeFor, countLeaves, createConversationBriefClient, createDraftMessageClient, createToastQueue, defaultValueFor, draftMessageCardStyles, draftMessageComposerStyles, draftOutcomeFor, emptyStateStyles, fieldControlStyles, fieldErrorStyles, fieldHintStyles, fieldLabelStyles, fileViewerFallbackCardStyles, fileViewerStyles, fitBadgeCount, formatFileSize, formatValue, getPlatform, glowFrameStyles, iconForContentType, imageAttachmentStyles, imageLightboxStyles, isColumnDragData, isDialableConversationBrief, isDraftReviewOutcome, isEditableTarget, isPreviewableImageName, joinFieldLabel, makeActionsColumn, messageComposerStyles, namesOf, navItemStyles, normalizeHexColor, operatorById, operatorsForType, parseCombo, progressBarStyles, progressRingStyles, publishDraftMessageOutcome, rebaseRowOnFieldChange, rebaseRowOnOperatorChange, recordUpdateRowStyles, recordUpdateStyles, registerDraftMessageActions, removeLeaf, removeSubgroup, replaceLeaf, resolveLucideIcon, resolveLucideIconName, segmentedControlStyles, setJoiner, stepperStyles, subscribeDraftMessageOutcome, summaryFor, tableCellStyles, tableHeaderCellStyles, tabsStyles, terminalKindFor, textShimmerStyles, timeSlotListStyles, toastStyles, updateLeaf, updateSubgroup, useActiveCommands, useAttributeTypeIcon, useCommand, useDiscoveryActive, useDraftMessageActions, useDraftMessageOutcome, useEscapeKey, useInfiniteScrollSentinel, useRenderChipItem, useRenderValueInput, useShowDetails, viewerKindForContentType, wrapLeafInGroup };
10350
+ export { ATTRIBUTE_TYPES, ATTRIBUTE_TYPE_ICONS, AttachmentChip, AttributePicker, AttributeType, AttributeTypeIcon, AvailabilityCalendar, Avatar, AvatarGroup, Badge, BadgeAction, BadgeGroup, BadgeMultiSelect, BooleanCell, Breadcrumbs, COLUMN_DRAG_KIND, CONVERSATION_BRIEF_OUTCOME_LABEL, Card, CardBody, CardContent, CardEyebrow, CardFeatureTitle, CardFooter, CardHeader, CardTitle, ChannelIcon, Checkbox, CheckboxGroup, ChipValue, ColorInput, Combobox, Command, CommandHint, ConversationBriefCard, ConversationBriefPreview, ConversationBriefReview, CurrencyInput, DRAFT_OUTCOME_LABEL, DataTable, DataTableEmpty, DataTableError, DataTableSkeleton, DateCell, DatePicker, DateTimeCell, DateTimePicker, DraftMessageCard, DraftMessageComposer, DraftMessageReview, EmptyState, ErrorAnimation, FIELD_LABEL_SEPARATOR, FieldCaret, FileField, FileViewer, FileViewerFallbackCard, FilterRow, FilterValueInput, FrameStrip, GlowFrame, IconPicker, ImageAttachment, ImageLightbox, Input, Kbd, LinkCell, MAX_COMPOSER_ATTACHMENT_BYTES, MAX_DECODE_BYTES, MediaControls, MediaMarkerTrack, MediaPlayer, MediaStage, MediaTimeline, MediaVideoStage, MediaWaveform, Menu, MenuItem, MenuPopover, MenuTrigger, MessageComposer, Modal, ModalBody, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalTitle, MultiSelect, NavGroup, NavItem, NavSection, OPERATORS, OperatorPicker, Orb, PROTECTED_COMBOS, Popover, PopoverContent, PopoverPanelBody, PopoverPanelFooter, PopoverPanelHeader, PressableCard, ProgressBar, ProgressRing, Radio, RadioGroup, RecipientSelect, RecordFilterBuilderPanel, RecordFilterButton, RecordFilterChips, RecordFilterEditor, RecordUpdate, RecordUpdateErrors, RecordUpdateFooter, RecordUpdateRow, RecordUpdateRows, RecordUpdateSummary, RenderChipItemProvider, RenderValueInputProvider, SELECTION_COLUMN_ID, SegmentedControl, Select, Separator, Slider, Stepper, StepperStep, SubmenuTrigger, SuccessAnimation, Switch, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableHeaderSpacer, TableRow, TableScroller, Tabs, TagInput, TextShimmer, Textarea, TimeField, TimeSlotList, ToastRegion, Tooltip, TooltipTrigger, TruncateCell, __resetDraftMessageActions, __resetDraftMessageOutcomes, addLeaf as addFilterLeaf, addPendingLeaf, addSubgroup, arityOf, attachmentChipStyles, availabilityCalendarStyles, avatarGroupStyles, avatarStyles, badgeGroupOverflowStyles, badgeGroupStyles, badgeStyles, cardStyles2 as cardStyles, channelIcon, contactPhoneNumbers, conversationBriefCardStyles, conversationBriefOutcomeFor, countLeaves, createConversationBriefClient, createDraftMessageClient, createToastQueue, currentMediaMarkers, decodePeaks, defaultValueFor, draftMessageCardStyles, draftMessageComposerStyles, draftOutcomeFor, emptyStateStyles, fieldControlStyles, fieldErrorStyles, fieldHintStyles, fieldLabelStyles, fileViewerFallbackCardStyles, fileViewerStyles, fitBadgeCount, fitMediaBox, formatFileSize, formatMediaTime, formatValue, frameStripStyles, getPlatform, glowFrameStyles, iconForContentType, imageAttachmentStyles, imageLightboxStyles, isColumnDragData, isDialableConversationBrief, isDraftReviewOutcome, isEditableTarget, isPreviewableImageName, joinFieldLabel, makeActionsColumn, maskPeaks, mediaControlsStyles, mediaMarkersStyles, mediaPlayerStyles, mediaTimelineStyles, mediaVideoStageStyles, mediaWaveformStyles, messageComposerStyles, namesOf, navItemStyles, normalizeHexColor, operatorById, operatorsForType, parseCombo, peaksFromChannels, progressBarStyles, progressRingStyles, publishDraftMessageOutcome, rebaseRowOnFieldChange, rebaseRowOnOperatorChange, recordUpdateRowStyles, recordUpdateStyles, registerDraftMessageActions, removeLeaf, removeSubgroup, replaceLeaf, resolveLucideIcon, resolveLucideIconName, segmentedControlStyles, setJoiner, sliderStyles, spokenMediaTime, stepperStyles, subscribeDraftMessageOutcome, summaryFor, tableCellStyles, tableHeaderCellStyles, tabsStyles, terminalKindFor, textShimmerStyles, timeSlotListStyles, toastStyles, updateLeaf, updateSubgroup, useActiveCommands, useAttributeTypeIcon, useCommand, useDiscoveryActive, useDraftMessageActions, useDraftMessageOutcome, useEscapeKey, useInfiniteScrollSentinel, useMediaPlayer, useRenderChipItem, useRenderValueInput, useShowDetails, viewerKindForContentType, wrapLeafInGroup };
9391
10351
  //# sourceMappingURL=index.js.map
9392
10352
  //# sourceMappingURL=index.js.map