react-audio-wavekit 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 {
@@ -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) => {
@@ -124,7 +169,8 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
124
169
  }
125
170
  return `${secs}초`;
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
  {
@@ -136,9 +182,10 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
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) => {
@@ -122,7 +167,8 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
122
167
  }
123
168
  return `${secs}초`;
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
  {
@@ -134,9 +180,10 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
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.0",
4
4
  "license": "CC0-1.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",