react-audio-wavekit 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,7 @@ Visualize existing audio files (mp3, wav, etc.) with playhead and seek support.
32
32
 
33
33
  ![AudioWaveform](https://react-audio-wavekit.netlify.app/audio-wave-form.png)
34
34
 
35
- Static waveform visualization with playhead and click-to-seek.
35
+ Static waveform visualization with playhead and drag-to-seek.
36
36
 
37
37
  [▶ Demo](https://react-audio-wavekit.netlify.app/?path=/story/waveform-audiowaveform--default)
38
38
 
@@ -41,7 +41,12 @@ Static waveform visualization with playhead and click-to-seek.
41
41
  blob={audioBlob}
42
42
  currentTime={currentTime}
43
43
  duration={duration}
44
- onSeek={(time) => (audioRef.current.currentTime = time)}
44
+ onSeekStart={() => audio.pause()}
45
+ onSeekDrag={(time) => setCurrentTime(time)}
46
+ onSeekEnd={(time) => {
47
+ audio.currentTime = time;
48
+ audio.play();
49
+ }}
45
50
  />
46
51
  ```
47
52
 
@@ -51,7 +56,10 @@ Static waveform visualization with playhead and click-to-seek.
51
56
  | `peaks` | `number[]` | - | Pre-computed peaks (0-1 range, skips decoding) |
52
57
  | `currentTime` | `number` | - | Current playback time in seconds |
53
58
  | `duration` | `number` | - | Total audio duration in seconds |
54
- | `onSeek` | `(time: number) => void` | - | Callback when user clicks on waveform |
59
+ | `onSeek` | `(time: number) => void` | - | Callback for simple click-to-seek |
60
+ | `onSeekStart` | `() => void` | - | Callback when drag starts (pause playback) |
61
+ | `onSeekDrag` | `(time: number) => void` | - | Callback during drag (real-time updates) |
62
+ | `onSeekEnd` | `(time: number) => void` | - | Callback when drag ends (resume playback) |
55
63
  | `suspense` | `boolean` | `false` | Enable React Suspense mode |
56
64
  | `appearance` | `AudioWaveformAppearance` | - | See [Appearance Options](#appearance-options) |
57
65
 
package/dist/index.d.ts CHANGED
@@ -29,8 +29,14 @@ declare interface AudioWaveformProps extends React.CanvasHTMLAttributes<HTMLCanv
29
29
  currentTime?: number;
30
30
  /** Total audio duration in seconds (required for playhead positioning) */
31
31
  duration?: number;
32
- /** Callback when user clicks/seeks on waveform */
32
+ /** Callback when user clicks/seeks on waveform (simple seek) */
33
33
  onSeek?: (time: number) => void;
34
+ /** Callback when drag-to-seek starts (use to pause playback) */
35
+ onSeekStart?: () => void;
36
+ /** Callback during drag-to-seek with current time (real-time updates) */
37
+ onSeekDrag?: (time: number) => void;
38
+ /** Callback when drag-to-seek ends (use to resume playback) */
39
+ onSeekEnd?: (time: number) => void;
34
40
  }
35
41
 
36
42
  declare interface AudioWaveformRef {
@@ -38,8 +44,8 @@ declare interface AudioWaveformRef {
38
44
  }
39
45
 
40
46
  /**
41
- * 실시간 오디오 주파수 시각화 컴포넌트
42
- * MediaRecorder 오디오를 Web Audio API 분석하여 형태로 렌더링
47
+ * Real-time audio frequency visualization component
48
+ * Analyzes MediaRecorder audio via Web Audio API and renders as bars
43
49
  */
44
50
  export declare const LiveRecorder: ForwardRefExoticComponent<LiveRecorderProps & RefAttributes<LiveRecorderRef>>;
45
51
 
@@ -102,7 +108,7 @@ declare interface LiveStreamingRecorderCanvasProps extends HTMLAttributes<HTMLCa
102
108
  className?: string;
103
109
  /** Inline styles for canvas element */
104
110
  style?: React.CSSProperties;
105
- /** Waveform appearance configuration (barColor, barWidth, etc.) - scrollbar 설정은 Root에서만 유효 */
111
+ /** Waveform appearance configuration (barColor, barWidth, etc.) - scrollbar settings only apply in Root */
106
112
  appearance?: LiveStreamingRecorderAppearance;
107
113
  }
108
114
 
@@ -30,7 +30,7 @@ const LiveStreamingRecorderRoot = react.forwardRef(
30
30
  theme: themeClassName,
31
31
  visibility: hidden ? "hidden" : "auto",
32
32
  autoHide: "leave",
33
- // 마우스가 영역을 벗어나면 숨김 (가장 대중적인 UX)
33
+ // Hide when mouse leaves area (most common UX)
34
34
  autoHideDelay: 400,
35
35
  dragScroll: true,
36
36
  clickScroll: true
@@ -28,7 +28,7 @@ const LiveStreamingRecorderRoot = forwardRef(
28
28
  theme: themeClassName,
29
29
  visibility: hidden ? "hidden" : "auto",
30
30
  autoHide: "leave",
31
- // 마우스가 영역을 벗어나면 숨김 (가장 대중적인 UX)
31
+ // Hide when mouse leaves area (most common UX)
32
32
  autoHideDelay: 400,
33
33
  dragScroll: true,
34
34
  clickScroll: true
@@ -9,7 +9,19 @@ const getInitialSampleCount = () => {
9
9
  if (typeof window === "undefined") return 500;
10
10
  return Math.max(500, Math.ceil(window.innerWidth));
11
11
  };
12
- const AudioWaveform = react.forwardRef(function AudioWaveform2({ blob, peaks: precomputedPeaks, appearance, suspense = false, currentTime, duration, onSeek, ...props }, ref) {
12
+ const AudioWaveform = react.forwardRef(function AudioWaveform2({
13
+ blob,
14
+ peaks: precomputedPeaks,
15
+ appearance,
16
+ suspense = false,
17
+ currentTime,
18
+ duration,
19
+ onSeek,
20
+ onSeekStart,
21
+ onSeekDrag,
22
+ onSeekEnd,
23
+ ...props
24
+ }, ref) {
13
25
  const [decodedPeaks, setDecodedPeaks] = react.useState(null);
14
26
  const [error, setError] = react.useState(null);
15
27
  const blobRef = react.useRef(null);
@@ -63,6 +75,9 @@ const AudioWaveform = react.forwardRef(function AudioWaveform2({ blob, peaks: pr
63
75
  currentTime,
64
76
  duration,
65
77
  onSeek,
78
+ onSeekStart,
79
+ onSeekDrag,
80
+ onSeekEnd,
66
81
  ...props
67
82
  }
68
83
  );
@@ -7,7 +7,19 @@ const getInitialSampleCount = () => {
7
7
  if (typeof window === "undefined") return 500;
8
8
  return Math.max(500, Math.ceil(window.innerWidth));
9
9
  };
10
- const AudioWaveform = forwardRef(function AudioWaveform2({ blob, peaks: precomputedPeaks, appearance, suspense = false, currentTime, duration, onSeek, ...props }, ref) {
10
+ const AudioWaveform = forwardRef(function AudioWaveform2({
11
+ blob,
12
+ peaks: precomputedPeaks,
13
+ appearance,
14
+ suspense = false,
15
+ currentTime,
16
+ duration,
17
+ onSeek,
18
+ onSeekStart,
19
+ onSeekDrag,
20
+ onSeekEnd,
21
+ ...props
22
+ }, ref) {
11
23
  const [decodedPeaks, setDecodedPeaks] = useState(null);
12
24
  const [error, setError] = useState(null);
13
25
  const blobRef = useRef(null);
@@ -61,6 +73,9 @@ const AudioWaveform = forwardRef(function AudioWaveform2({ blob, peaks: precompu
61
73
  currentTime,
62
74
  duration,
63
75
  onSeek,
76
+ onSeekStart,
77
+ onSeekDrag,
78
+ onSeekEnd,
64
79
  ...props
65
80
  }
66
81
  );
@@ -3,10 +3,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
5
  const constants = require("../constants.cjs");
6
- const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onClick, style, ...props }, ref) {
6
+ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onSeekStart, onSeekDrag, onSeekEnd, onClick, style, ...props }, ref) {
7
7
  const canvasRef = react.useRef(null);
8
8
  const sizeRef = react.useRef({ width: 0, height: 0 });
9
9
  const rafRef = react.useRef(0);
10
+ const isDraggingRef = react.useRef(false);
10
11
  react.useImperativeHandle(ref, () => ({
11
12
  canvas: canvasRef.current
12
13
  }));
@@ -77,18 +78,62 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
77
78
  react.useEffect(() => {
78
79
  drawWaveform();
79
80
  }, [drawWaveform]);
81
+ const getTimeFromPosition = react.useCallback(
82
+ (clientX) => {
83
+ const canvas = canvasRef.current;
84
+ if (!canvas || !duration || duration <= 0) return 0;
85
+ const rect = canvas.getBoundingClientRect();
86
+ const x = clientX - rect.left;
87
+ const ratio = Math.max(0, Math.min(x / rect.width, 1));
88
+ return ratio * duration;
89
+ },
90
+ [duration]
91
+ );
92
+ react.useEffect(() => {
93
+ const handleMouseMove = (e) => {
94
+ if (!isDraggingRef.current) return;
95
+ const time = getTimeFromPosition(e.clientX);
96
+ onSeekDrag?.(time);
97
+ };
98
+ const handleMouseUp = (e) => {
99
+ if (!isDraggingRef.current) return;
100
+ isDraggingRef.current = false;
101
+ document.body.style.cursor = "";
102
+ document.body.style.userSelect = "";
103
+ const time = getTimeFromPosition(e.clientX);
104
+ onSeekEnd?.(time);
105
+ };
106
+ if (onSeekDrag || onSeekEnd) {
107
+ document.addEventListener("mousemove", handleMouseMove);
108
+ document.addEventListener("mouseup", handleMouseUp);
109
+ }
110
+ return () => {
111
+ document.removeEventListener("mousemove", handleMouseMove);
112
+ document.removeEventListener("mouseup", handleMouseUp);
113
+ };
114
+ }, [getTimeFromPosition, onSeekDrag, onSeekEnd]);
115
+ const handleMouseDown = react.useCallback(
116
+ (e) => {
117
+ if (!duration || duration <= 0) return;
118
+ if (onSeekStart || onSeekDrag || onSeekEnd) {
119
+ isDraggingRef.current = true;
120
+ document.body.style.cursor = "grabbing";
121
+ document.body.style.userSelect = "none";
122
+ onSeekStart?.();
123
+ const time = getTimeFromPosition(e.clientX);
124
+ onSeekDrag?.(time);
125
+ }
126
+ },
127
+ [duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
128
+ );
80
129
  const handleClick = react.useCallback(
81
130
  (e) => {
131
+ if (onSeekStart || onSeekDrag || onSeekEnd) return;
82
132
  if (!onSeek || !duration || duration <= 0) return;
83
- const canvas = canvasRef.current;
84
- if (!canvas) return;
85
- const rect = canvas.getBoundingClientRect();
86
- const x = e.clientX - rect.left;
87
- const clickRatio = x / rect.width;
88
- const newTime = Math.max(0, Math.min(clickRatio * duration, duration));
89
- onSeek(newTime);
133
+ const time = getTimeFromPosition(e.clientX);
134
+ onSeek(time);
90
135
  },
91
- [onSeek, duration]
136
+ [onSeek, duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
92
137
  );
93
138
  const handleKeyDown = react.useCallback(
94
139
  (e) => {
@@ -120,25 +165,27 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
120
165
  const mins = Math.floor(seconds / 60);
121
166
  const secs = Math.floor(seconds % 60);
122
167
  if (mins > 0) {
123
- return `${mins} ${secs}초`;
168
+ return `${mins} minute${mins > 1 ? "s" : ""} ${secs} second${secs !== 1 ? "s" : ""}`;
124
169
  }
125
- return `${secs}초`;
170
+ return `${secs} second${secs !== 1 ? "s" : ""}`;
126
171
  };
127
- const isInteractive = !!onSeek && !!duration && duration > 0;
172
+ const isInteractive = (!!onSeek || !!onSeekStart || !!onSeekDrag || !!onSeekEnd) && !!duration && duration > 0;
173
+ const isDragEnabled = !!onSeekStart || !!onSeekDrag || !!onSeekEnd;
128
174
  return /* @__PURE__ */ jsxRuntime.jsx(
129
175
  "canvas",
130
176
  {
131
177
  ref: canvasRef,
132
178
  role: isInteractive ? "slider" : "img",
133
- "aria-label": isInteractive ? "오디오 탐색" : "오디오 파형",
179
+ "aria-label": isInteractive ? "Audio seek" : "Audio waveform",
134
180
  "aria-valuemin": isInteractive ? 0 : void 0,
135
181
  "aria-valuemax": isInteractive ? Math.floor(duration) : void 0,
136
182
  "aria-valuenow": isInteractive ? Math.floor(currentTime ?? 0) : void 0,
137
183
  "aria-valuetext": isInteractive ? `${formatTimeForScreen(currentTime ?? 0)} / ${formatTimeForScreen(duration)}` : void 0,
138
184
  tabIndex: isInteractive ? 0 : -1,
185
+ onMouseDown: isDragEnabled ? handleMouseDown : void 0,
139
186
  onClick: handleClick,
140
187
  onKeyDown: isInteractive ? handleKeyDown : void 0,
141
- style: { cursor: isInteractive ? "pointer" : void 0, ...style },
188
+ style: { cursor: isInteractive ? isDragEnabled ? "grab" : "pointer" : void 0, ...style },
142
189
  ...props
143
190
  }
144
191
  );
@@ -1,10 +1,11 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect } from "react";
3
3
  import { DEFAULT_WAVEFORM_APPEARANCE, DEFAULT_PLAYHEAD_APPEARANCE } from "../constants.js";
4
- const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onClick, style, ...props }, ref) {
4
+ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onSeekStart, onSeekDrag, onSeekEnd, onClick, style, ...props }, ref) {
5
5
  const canvasRef = useRef(null);
6
6
  const sizeRef = useRef({ width: 0, height: 0 });
7
7
  const rafRef = useRef(0);
8
+ const isDraggingRef = useRef(false);
8
9
  useImperativeHandle(ref, () => ({
9
10
  canvas: canvasRef.current
10
11
  }));
@@ -75,18 +76,62 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
75
76
  useEffect(() => {
76
77
  drawWaveform();
77
78
  }, [drawWaveform]);
79
+ const getTimeFromPosition = useCallback(
80
+ (clientX) => {
81
+ const canvas = canvasRef.current;
82
+ if (!canvas || !duration || duration <= 0) return 0;
83
+ const rect = canvas.getBoundingClientRect();
84
+ const x = clientX - rect.left;
85
+ const ratio = Math.max(0, Math.min(x / rect.width, 1));
86
+ return ratio * duration;
87
+ },
88
+ [duration]
89
+ );
90
+ useEffect(() => {
91
+ const handleMouseMove = (e) => {
92
+ if (!isDraggingRef.current) return;
93
+ const time = getTimeFromPosition(e.clientX);
94
+ onSeekDrag?.(time);
95
+ };
96
+ const handleMouseUp = (e) => {
97
+ if (!isDraggingRef.current) return;
98
+ isDraggingRef.current = false;
99
+ document.body.style.cursor = "";
100
+ document.body.style.userSelect = "";
101
+ const time = getTimeFromPosition(e.clientX);
102
+ onSeekEnd?.(time);
103
+ };
104
+ if (onSeekDrag || onSeekEnd) {
105
+ document.addEventListener("mousemove", handleMouseMove);
106
+ document.addEventListener("mouseup", handleMouseUp);
107
+ }
108
+ return () => {
109
+ document.removeEventListener("mousemove", handleMouseMove);
110
+ document.removeEventListener("mouseup", handleMouseUp);
111
+ };
112
+ }, [getTimeFromPosition, onSeekDrag, onSeekEnd]);
113
+ const handleMouseDown = useCallback(
114
+ (e) => {
115
+ if (!duration || duration <= 0) return;
116
+ if (onSeekStart || onSeekDrag || onSeekEnd) {
117
+ isDraggingRef.current = true;
118
+ document.body.style.cursor = "grabbing";
119
+ document.body.style.userSelect = "none";
120
+ onSeekStart?.();
121
+ const time = getTimeFromPosition(e.clientX);
122
+ onSeekDrag?.(time);
123
+ }
124
+ },
125
+ [duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
126
+ );
78
127
  const handleClick = useCallback(
79
128
  (e) => {
129
+ if (onSeekStart || onSeekDrag || onSeekEnd) return;
80
130
  if (!onSeek || !duration || duration <= 0) return;
81
- const canvas = canvasRef.current;
82
- if (!canvas) return;
83
- const rect = canvas.getBoundingClientRect();
84
- const x = e.clientX - rect.left;
85
- const clickRatio = x / rect.width;
86
- const newTime = Math.max(0, Math.min(clickRatio * duration, duration));
87
- onSeek(newTime);
131
+ const time = getTimeFromPosition(e.clientX);
132
+ onSeek(time);
88
133
  },
89
- [onSeek, duration]
134
+ [onSeek, duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
90
135
  );
91
136
  const handleKeyDown = useCallback(
92
137
  (e) => {
@@ -118,25 +163,27 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
118
163
  const mins = Math.floor(seconds / 60);
119
164
  const secs = Math.floor(seconds % 60);
120
165
  if (mins > 0) {
121
- return `${mins} ${secs}초`;
166
+ return `${mins} minute${mins > 1 ? "s" : ""} ${secs} second${secs !== 1 ? "s" : ""}`;
122
167
  }
123
- return `${secs}초`;
168
+ return `${secs} second${secs !== 1 ? "s" : ""}`;
124
169
  };
125
- const isInteractive = !!onSeek && !!duration && duration > 0;
170
+ const isInteractive = (!!onSeek || !!onSeekStart || !!onSeekDrag || !!onSeekEnd) && !!duration && duration > 0;
171
+ const isDragEnabled = !!onSeekStart || !!onSeekDrag || !!onSeekEnd;
126
172
  return /* @__PURE__ */ jsx(
127
173
  "canvas",
128
174
  {
129
175
  ref: canvasRef,
130
176
  role: isInteractive ? "slider" : "img",
131
- "aria-label": isInteractive ? "오디오 탐색" : "오디오 파형",
177
+ "aria-label": isInteractive ? "Audio seek" : "Audio waveform",
132
178
  "aria-valuemin": isInteractive ? 0 : void 0,
133
179
  "aria-valuemax": isInteractive ? Math.floor(duration) : void 0,
134
180
  "aria-valuenow": isInteractive ? Math.floor(currentTime ?? 0) : void 0,
135
181
  "aria-valuetext": isInteractive ? `${formatTimeForScreen(currentTime ?? 0)} / ${formatTimeForScreen(duration)}` : void 0,
136
182
  tabIndex: isInteractive ? 0 : -1,
183
+ onMouseDown: isDragEnabled ? handleMouseDown : void 0,
137
184
  onClick: handleClick,
138
185
  onKeyDown: isInteractive ? handleKeyDown : void 0,
139
- style: { cursor: isInteractive ? "pointer" : void 0, ...style },
186
+ style: { cursor: isInteractive ? isDragEnabled ? "grab" : "pointer" : void 0, ...style },
140
187
  ...props
141
188
  }
142
189
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-audio-wavekit",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "license": "CC0-1.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -42,7 +42,9 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=18.0.0",
45
- "react-dom": ">=18.0.0"
45
+ "react-dom": ">=18.0.0",
46
+ "overlayscrollbars": "^2.0.0",
47
+ "overlayscrollbars-react": "^0.5.0"
46
48
  },
47
49
  "devDependencies": {
48
50
  "@biomejs/biome": "2.3.8",
@@ -68,8 +70,6 @@
68
70
  "vitest": "^4.0.15"
69
71
  },
70
72
  "dependencies": {
71
- "mpg123-decoder": "^1.0.3",
72
- "overlayscrollbars": "^2.13.0",
73
- "overlayscrollbars-react": "^0.5.6"
73
+ "mpg123-decoder": "^1.0.3"
74
74
  }
75
75
  }