{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waveform",
  "files": [
    {
      "path": "components/ui/waveform.tsx",
      "content": "\"use client\"\n\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type HTMLAttributes,\n} from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type WaveformProps = HTMLAttributes<HTMLDivElement> & {\n  data?: number[]\n  barWidth?: number\n  barHeight?: number\n  barGap?: number\n  barRadius?: number\n  barColor?: string\n  fadeEdges?: boolean\n  fadeWidth?: number\n  height?: string | number\n  active?: boolean\n  onBarClick?: (index: number, value: number) => void\n}\n\nexport const Waveform = ({\n  data = [],\n  barWidth = 4,\n  barHeight: baseBarHeight = 4,\n  barGap = 2,\n  barRadius = 2,\n  barColor,\n  fadeEdges = true,\n  fadeWidth = 24,\n  height = 128,\n  onBarClick,\n  className,\n  ...props\n}: WaveformProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const heightStyle = typeof height === \"number\" ? `${height}px` : height\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const container = containerRef.current\n    if (!canvas || !container) return\n\n    const resizeObserver = new ResizeObserver(() => {\n      const rect = container.getBoundingClientRect()\n      const dpr = window.devicePixelRatio || 1\n\n      canvas.width = rect.width * dpr\n      canvas.height = rect.height * dpr\n      canvas.style.width = `${rect.width}px`\n      canvas.style.height = `${rect.height}px`\n\n      const ctx = canvas.getContext(\"2d\")\n      if (ctx) {\n        ctx.scale(dpr, dpr)\n        renderWaveform()\n      }\n    })\n\n    const renderWaveform = () => {\n      const ctx = canvas.getContext(\"2d\")\n      if (!ctx) return\n\n      const rect = canvas.getBoundingClientRect()\n      ctx.clearRect(0, 0, rect.width, rect.height)\n\n      const computedBarColor =\n        barColor ||\n        getComputedStyle(canvas).getPropertyValue(\"--foreground\") ||\n        \"#000\"\n\n      const barCount = Math.floor(rect.width / (barWidth + barGap))\n      const centerY = rect.height / 2\n\n      for (let i = 0; i < barCount; i++) {\n        const dataIndex = Math.floor((i / barCount) * data.length)\n        const value = data[dataIndex] || 0\n        const barHeight = Math.max(baseBarHeight, value * rect.height * 0.8)\n        const x = i * (barWidth + barGap)\n        const y = centerY - barHeight / 2\n\n        ctx.fillStyle = computedBarColor\n        ctx.globalAlpha = 0.3 + value * 0.7\n\n        if (barRadius > 0) {\n          ctx.beginPath()\n          ctx.roundRect(x, y, barWidth, barHeight, barRadius)\n          ctx.fill()\n        } else {\n          ctx.fillRect(x, y, barWidth, barHeight)\n        }\n      }\n\n      if (fadeEdges && fadeWidth > 0 && rect.width > 0) {\n        const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)\n        const fadePercent = Math.min(0.2, fadeWidth / rect.width)\n\n        gradient.addColorStop(0, \"rgba(255,255,255,1)\")\n        gradient.addColorStop(fadePercent, \"rgba(255,255,255,0)\")\n        gradient.addColorStop(1 - fadePercent, \"rgba(255,255,255,0)\")\n        gradient.addColorStop(1, \"rgba(255,255,255,1)\")\n\n        ctx.globalCompositeOperation = \"destination-out\"\n        ctx.fillStyle = gradient\n        ctx.fillRect(0, 0, rect.width, rect.height)\n        ctx.globalCompositeOperation = \"source-over\"\n      }\n\n      ctx.globalAlpha = 1\n    }\n\n    resizeObserver.observe(container)\n    renderWaveform()\n\n    return () => resizeObserver.disconnect()\n  }, [\n    data,\n    barWidth,\n    baseBarHeight,\n    barGap,\n    barRadius,\n    barColor,\n    fadeEdges,\n    fadeWidth,\n  ])\n\n  const handleClick = (e: React.MouseEvent<HTMLCanvasElement>) => {\n    if (!onBarClick) return\n\n    const rect = canvasRef.current?.getBoundingClientRect()\n    if (!rect) return\n\n    const x = e.clientX - rect.left\n    const barIndex = Math.floor(x / (barWidth + barGap))\n    const dataIndex = Math.floor(\n      (barIndex * data.length) / Math.floor(rect.width / (barWidth + barGap))\n    )\n\n    if (dataIndex >= 0 && dataIndex < data.length) {\n      onBarClick(dataIndex, data[dataIndex])\n    }\n  }\n\n  return (\n    <div\n      className={cn(\"relative\", className)}\n      ref={containerRef}\n      style={{ height: heightStyle }}\n      {...props}\n    >\n      <canvas\n        className=\"block h-full w-full\"\n        onClick={handleClick}\n        ref={canvasRef}\n      />\n    </div>\n  )\n}\n\nexport type ScrollingWaveformProps = Omit<\n  WaveformProps,\n  \"data\" | \"onBarClick\"\n> & {\n  speed?: number\n  barCount?: number\n  data?: number[]\n}\n\nexport const ScrollingWaveform = ({\n  speed = 50,\n  barCount = 60,\n  barWidth = 4,\n  barHeight: baseBarHeight = 4,\n  barGap = 2,\n  barRadius = 2,\n  barColor,\n  fadeEdges = true,\n  fadeWidth = 24,\n  height = 128,\n  data,\n  className,\n  ...props\n}: ScrollingWaveformProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const barsRef = useRef<Array<{ x: number; height: number }>>([])\n  const animationRef = useRef<number>(0)\n  const lastTimeRef = useRef<number>(0)\n  const seedRef = useRef(Math.random())\n  const dataIndexRef = useRef(0)\n  const heightStyle = typeof height === \"number\" ? `${height}px` : height\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const container = containerRef.current\n    if (!canvas || !container) return\n\n    const resizeObserver = new ResizeObserver(() => {\n      const rect = container.getBoundingClientRect()\n      const dpr = window.devicePixelRatio || 1\n\n      canvas.width = rect.width * dpr\n      canvas.height = rect.height * dpr\n      canvas.style.width = `${rect.width}px`\n      canvas.style.height = `${rect.height}px`\n\n      const ctx = canvas.getContext(\"2d\")\n      if (ctx) {\n        ctx.scale(dpr, dpr)\n      }\n\n      if (barsRef.current.length === 0) {\n        const step = barWidth + barGap\n        let currentX = rect.width\n        let index = 0\n        const seededRandom = (i: number) => {\n          const x = Math.sin(seedRef.current * 10000 + i) * 10000\n          return x - Math.floor(x)\n        }\n        while (currentX > -step) {\n          barsRef.current.push({\n            x: currentX,\n            height: 0.2 + seededRandom(index++) * 0.6,\n          })\n          currentX -= step\n        }\n      }\n    })\n\n    resizeObserver.observe(container)\n    return () => resizeObserver.disconnect()\n  }, [barWidth, barGap])\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    const animate = (currentTime: number) => {\n      const deltaTime = lastTimeRef.current\n        ? (currentTime - lastTimeRef.current) / 1000\n        : 0\n      lastTimeRef.current = currentTime\n\n      const rect = canvas.getBoundingClientRect()\n      ctx.clearRect(0, 0, rect.width, rect.height)\n\n      const computedBarColor =\n        barColor ||\n        getComputedStyle(canvas).getPropertyValue(\"--foreground\") ||\n        \"#000\"\n\n      const step = barWidth + barGap\n      for (let i = 0; i < barsRef.current.length; i++) {\n        barsRef.current[i].x -= speed * deltaTime\n      }\n\n      barsRef.current = barsRef.current.filter(\n        (bar) => bar.x + barWidth > -step\n      )\n\n      while (\n        barsRef.current.length === 0 ||\n        barsRef.current[barsRef.current.length - 1].x < rect.width\n      ) {\n        const lastBar = barsRef.current[barsRef.current.length - 1]\n        const nextX = lastBar ? lastBar.x + step : rect.width\n\n        let newHeight: number\n        if (data && data.length > 0) {\n          newHeight = data[dataIndexRef.current % data.length] || 0.1\n          dataIndexRef.current = (dataIndexRef.current + 1) % data.length\n        } else {\n          const time = Date.now() / 1000\n          const uniqueIndex = barsRef.current.length + time * 0.01\n          const seededRandom = (index: number) => {\n            const x = Math.sin(seedRef.current * 10000 + index * 137.5) * 10000\n            return x - Math.floor(x)\n          }\n          const wave1 = Math.sin(uniqueIndex * 0.1) * 0.2\n          const wave2 = Math.cos(uniqueIndex * 0.05) * 0.15\n          const randomComponent = seededRandom(uniqueIndex) * 0.4\n          newHeight = Math.max(\n            0.1,\n            Math.min(0.9, 0.3 + wave1 + wave2 + randomComponent)\n          )\n        }\n\n        barsRef.current.push({\n          x: nextX,\n          height: newHeight,\n        })\n        if (barsRef.current.length > barCount * 2) break\n      }\n\n      const centerY = rect.height / 2\n      for (const bar of barsRef.current) {\n        if (bar.x < rect.width && bar.x + barWidth > 0) {\n          const barHeight = Math.max(\n            baseBarHeight,\n            bar.height * rect.height * 0.6\n          )\n          const y = centerY - barHeight / 2\n\n          ctx.fillStyle = computedBarColor\n          ctx.globalAlpha = 0.3 + bar.height * 0.7\n\n          if (barRadius > 0) {\n            ctx.beginPath()\n            ctx.roundRect(bar.x, y, barWidth, barHeight, barRadius)\n            ctx.fill()\n          } else {\n            ctx.fillRect(bar.x, y, barWidth, barHeight)\n          }\n        }\n      }\n\n      if (fadeEdges && fadeWidth > 0) {\n        const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)\n        const fadePercent = Math.min(0.2, fadeWidth / rect.width)\n\n        gradient.addColorStop(0, \"rgba(255,255,255,1)\")\n        gradient.addColorStop(fadePercent, \"rgba(255,255,255,0)\")\n        gradient.addColorStop(1 - fadePercent, \"rgba(255,255,255,0)\")\n        gradient.addColorStop(1, \"rgba(255,255,255,1)\")\n\n        ctx.globalCompositeOperation = \"destination-out\"\n        ctx.fillStyle = gradient\n        ctx.fillRect(0, 0, rect.width, rect.height)\n        ctx.globalCompositeOperation = \"source-over\"\n      }\n\n      ctx.globalAlpha = 1\n\n      animationRef.current = requestAnimationFrame(animate)\n    }\n\n    animationRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n      }\n    }\n  }, [\n    speed,\n    barCount,\n    barWidth,\n    baseBarHeight,\n    barGap,\n    barRadius,\n    barColor,\n    fadeEdges,\n    fadeWidth,\n    data,\n  ])\n\n  return (\n    <div\n      className={cn(\"relative flex items-center\", className)}\n      ref={containerRef}\n      style={{ height: heightStyle }}\n      {...props}\n    >\n      <canvas className=\"block h-full w-full\" ref={canvasRef} />\n    </div>\n  )\n}\n\nexport type AudioScrubberProps = WaveformProps & {\n  currentTime?: number\n  duration?: number\n  onSeek?: (time: number) => void\n  showHandle?: boolean\n}\n\nexport const AudioScrubber = ({\n  data = [],\n  currentTime = 0,\n  duration = 100,\n  onSeek,\n  showHandle = true,\n  barWidth = 3,\n  barHeight,\n  barGap = 1,\n  barRadius = 1,\n  barColor,\n  height = 128,\n  className,\n  ...props\n}: AudioScrubberProps) => {\n  const [isDragging, setIsDragging] = useState(false)\n  const [localProgress, setLocalProgress] = useState(0)\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  const waveformData =\n    data.length > 0\n      ? data\n      : Array.from({ length: 100 }, () => 0.2 + Math.random() * 0.6)\n\n  useEffect(() => {\n    if (!isDragging && duration > 0) {\n      setLocalProgress(currentTime / duration)\n    }\n  }, [currentTime, duration, isDragging])\n\n  const handleScrub = useCallback(\n    (clientX: number) => {\n      const container = containerRef.current\n      if (!container) return\n\n      const rect = container.getBoundingClientRect()\n      const x = Math.max(0, Math.min(clientX - rect.left, rect.width))\n      const progress = x / rect.width\n      const newTime = progress * duration\n\n      setLocalProgress(progress)\n      onSeek?.(newTime)\n    },\n    [duration, onSeek]\n  )\n\n  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {\n    e.preventDefault()\n    setIsDragging(true)\n    handleScrub(e.clientX)\n  }\n\n  useEffect(() => {\n    if (!isDragging) return\n\n    const handleMouseMove = (e: MouseEvent) => {\n      handleScrub(e.clientX)\n    }\n\n    const handleMouseUp = () => {\n      setIsDragging(false)\n    }\n\n    document.addEventListener(\"mousemove\", handleMouseMove)\n    document.addEventListener(\"mouseup\", handleMouseUp)\n\n    return () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove)\n      document.removeEventListener(\"mouseup\", handleMouseUp)\n    }\n  }, [isDragging, duration, handleScrub])\n\n  const heightStyle = typeof height === \"number\" ? `${height}px` : height\n\n  return (\n    <div\n      aria-label=\"Audio waveform scrubber\"\n      aria-valuemax={duration}\n      aria-valuemin={0}\n      aria-valuenow={currentTime}\n      className={cn(\"relative cursor-pointer select-none\", className)}\n      onMouseDown={handleMouseDown}\n      ref={containerRef}\n      role=\"slider\"\n      style={{ height: heightStyle }}\n      tabIndex={0}\n      {...props}\n    >\n      <Waveform\n        barColor={barColor}\n        barGap={barGap}\n        barRadius={barRadius}\n        barWidth={barWidth}\n        barHeight={barHeight}\n        data={waveformData}\n        fadeEdges={false}\n      />\n\n      <div\n        className=\"bg-primary/20 pointer-events-none absolute inset-y-0 left-0\"\n        style={{ width: `${localProgress * 100}%` }}\n      />\n\n      <div\n        className=\"bg-primary pointer-events-none absolute top-0 bottom-0 w-0.5\"\n        style={{ left: `${localProgress * 100}%` }}\n      />\n\n      {showHandle && (\n        <div\n          className=\"border-background bg-primary pointer-events-none absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 shadow-lg transition-transform hover:scale-110\"\n          style={{ left: `${localProgress * 100}%` }}\n        />\n      )}\n    </div>\n  )\n}\n\nexport type MicrophoneWaveformProps = WaveformProps & {\n  active?: boolean\n  processing?: boolean\n  fftSize?: number\n  smoothingTimeConstant?: number\n  sensitivity?: number\n  onError?: (error: Error) => void\n}\n\nexport const MicrophoneWaveform = ({\n  active = false,\n  processing = false,\n  fftSize = 256,\n  smoothingTimeConstant = 0.8,\n  sensitivity = 1,\n  onError,\n  ...props\n}: MicrophoneWaveformProps) => {\n  const [data, setData] = useState<number[]>([])\n  const analyserRef = useRef<AnalyserNode | null>(null)\n  const audioContextRef = useRef<AudioContext | null>(null)\n  const streamRef = useRef<MediaStream | null>(null)\n  const animationIdRef = useRef<number | null>(null)\n  const processingAnimationRef = useRef<number | null>(null)\n  const lastActiveDataRef = useRef<number[]>([])\n  const transitionProgressRef = useRef(0)\n\n  useEffect(() => {\n    if (processing && !active) {\n      let time = 0\n      transitionProgressRef.current = 0\n\n      const animateProcessing = () => {\n        time += 0.03\n        transitionProgressRef.current = Math.min(\n          1,\n          transitionProgressRef.current + 0.02\n        )\n\n        const processingData = []\n        const barCount = 45\n\n        for (let i = 0; i < barCount; i++) {\n          const normalizedPosition = (i - barCount / 2) / (barCount / 2)\n          const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4\n\n          const wave1 = Math.sin(time * 1.5 + i * 0.15) * 0.25\n          const wave2 = Math.sin(time * 0.8 - i * 0.1) * 0.2\n          const wave3 = Math.cos(time * 2 + i * 0.05) * 0.15\n          const combinedWave = wave1 + wave2 + wave3\n          const processingValue = (0.2 + combinedWave) * centerWeight\n\n          let finalValue = processingValue\n          if (\n            lastActiveDataRef.current.length > 0 &&\n            transitionProgressRef.current < 1\n          ) {\n            const lastDataIndex = Math.floor(\n              (i / barCount) * lastActiveDataRef.current.length\n            )\n            const lastValue = lastActiveDataRef.current[lastDataIndex] || 0\n            finalValue =\n              lastValue * (1 - transitionProgressRef.current) +\n              processingValue * transitionProgressRef.current\n          }\n\n          processingData.push(Math.max(0.05, Math.min(1, finalValue)))\n        }\n\n        setData(processingData)\n        processingAnimationRef.current =\n          requestAnimationFrame(animateProcessing)\n      }\n\n      animateProcessing()\n\n      return () => {\n        if (processingAnimationRef.current) {\n          cancelAnimationFrame(processingAnimationRef.current)\n        }\n      }\n    } else if (!active && !processing) {\n      if (data.length > 0) {\n        let fadeProgress = 0\n        const fadeToIdle = () => {\n          fadeProgress += 0.03\n          if (fadeProgress < 1) {\n            const fadedData = data.map((value) => value * (1 - fadeProgress))\n            setData(fadedData)\n            requestAnimationFrame(fadeToIdle)\n          } else {\n            setData([])\n          }\n        }\n        fadeToIdle()\n      }\n      return\n    }\n  }, [processing, active])\n\n  useEffect(() => {\n    if (!active) {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n      }\n      if (\n        audioContextRef.current &&\n        audioContextRef.current.state !== \"closed\"\n      ) {\n        audioContextRef.current.close()\n      }\n      if (animationIdRef.current) {\n        cancelAnimationFrame(animationIdRef.current)\n      }\n      return\n    }\n\n    const setupMicrophone = async () => {\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({\n          audio: true,\n        })\n        streamRef.current = stream\n\n        const audioContext = new (window.AudioContext ||\n          (window as unknown as { webkitAudioContext: typeof AudioContext })\n            .webkitAudioContext)()\n        const analyser = audioContext.createAnalyser()\n        analyser.fftSize = fftSize\n        analyser.smoothingTimeConstant = smoothingTimeConstant\n\n        const source = audioContext.createMediaStreamSource(stream)\n        source.connect(analyser)\n\n        audioContextRef.current = audioContext\n        analyserRef.current = analyser\n\n        const dataArray = new Uint8Array(analyser.frequencyBinCount)\n\n        const updateData = () => {\n          if (!analyserRef.current || !active) return\n\n          analyserRef.current.getByteFrequencyData(dataArray)\n\n          const startFreq = Math.floor(dataArray.length * 0.05)\n          const endFreq = Math.floor(dataArray.length * 0.4)\n          const relevantData = dataArray.slice(startFreq, endFreq)\n\n          const halfLength = Math.floor(relevantData.length / 2)\n          const normalizedData = []\n\n          for (let i = halfLength - 1; i >= 0; i--) {\n            const value = Math.min(1, (relevantData[i] / 255) * sensitivity)\n            normalizedData.push(value)\n          }\n\n          for (let i = 0; i < halfLength; i++) {\n            const value = Math.min(1, (relevantData[i] / 255) * sensitivity)\n            normalizedData.push(value)\n          }\n\n          setData(normalizedData)\n          lastActiveDataRef.current = normalizedData\n\n          animationIdRef.current = requestAnimationFrame(updateData)\n        }\n\n        updateData()\n      } catch (error) {\n        onError?.(error as Error)\n      }\n    }\n\n    setupMicrophone()\n\n    return () => {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n      }\n      if (\n        audioContextRef.current &&\n        audioContextRef.current.state !== \"closed\"\n      ) {\n        audioContextRef.current.close()\n      }\n      if (animationIdRef.current) {\n        cancelAnimationFrame(animationIdRef.current)\n      }\n    }\n  }, [active, fftSize, smoothingTimeConstant, sensitivity, onError])\n\n  return <Waveform data={data} {...props} />\n}\n\nexport type StaticWaveformProps = WaveformProps & {\n  bars?: number\n  seed?: number\n}\n\nexport const StaticWaveform = ({\n  bars = 40,\n  seed = 42,\n  ...props\n}: StaticWaveformProps) => {\n  const data = useMemo(() => {\n    const random = (seedValue: number) => {\n      const x = Math.sin(seedValue) * 10000\n      return x - Math.floor(x)\n    }\n\n    return Array.from({ length: bars }, (_, i) => 0.2 + random(seed + i) * 0.6)\n  }, [bars, seed])\n\n  return <Waveform data={data} {...props} />\n}\n\nexport type LiveMicrophoneWaveformProps = Omit<\n  ScrollingWaveformProps,\n  \"barCount\"\n> & {\n  active?: boolean\n  fftSize?: number\n  smoothingTimeConstant?: number\n  sensitivity?: number\n  onError?: (error: Error) => void\n  historySize?: number\n  updateRate?: number\n  savedHistoryRef?: React.MutableRefObject<number[]>\n  dragOffset?: number\n  setDragOffset?: (offset: number) => void\n  enableAudioPlayback?: boolean\n  playbackRate?: number\n}\n\nexport const LiveMicrophoneWaveform = ({\n  active = false,\n  fftSize = 256,\n  smoothingTimeConstant = 0.8,\n  sensitivity = 1,\n  onError,\n  historySize = 150,\n  updateRate = 50,\n  barWidth = 3,\n  barHeight: baseBarHeight = 4,\n  barGap = 1,\n  barRadius = 1,\n  barColor,\n  fadeEdges = true,\n  fadeWidth = 24,\n  height = 128,\n  className,\n  savedHistoryRef,\n  dragOffset: externalDragOffset,\n  setDragOffset: externalSetDragOffset,\n  enableAudioPlayback = true,\n  playbackRate = 1,\n  ...props\n}: LiveMicrophoneWaveformProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const internalHistoryRef = useRef<number[]>([])\n  const historyRef = savedHistoryRef || internalHistoryRef\n  const analyserRef = useRef<AnalyserNode | null>(null)\n  const audioContextRef = useRef<AudioContext | null>(null)\n  const streamRef = useRef<MediaStream | null>(null)\n  const animationRef = useRef<number>(0)\n  const lastUpdateRef = useRef<number>(0)\n  const [internalDragOffset, setInternalDragOffset] = useState(0)\n  const [isDragging, setIsDragging] = useState(false)\n  const [playbackPosition, setPlaybackPosition] = useState<number | null>(null)\n  const dragStartXRef = useRef<number>(0)\n  const dragStartOffsetRef = useRef<number>(0)\n  const playbackStartTimeRef = useRef<number>(0)\n\n  // Audio recording and playback refs\n  const mediaRecorderRef = useRef<MediaRecorder | null>(null)\n  const audioChunksRef = useRef<Blob[]>([])\n  const audioBufferRef = useRef<AudioBuffer | null>(null)\n  const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null)\n  const scrubSourceRef = useRef<AudioBufferSourceNode | null>(null)\n\n  // Use external drag state if provided, otherwise use internal\n  const dragOffset = externalDragOffset ?? internalDragOffset\n  const setDragOffset = externalSetDragOffset ?? setInternalDragOffset\n\n  const heightStyle = typeof height === \"number\" ? `${height}px` : height\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const container = containerRef.current\n    if (!canvas || !container) return\n\n    const resizeObserver = new ResizeObserver(() => {\n      const rect = container.getBoundingClientRect()\n      const dpr = window.devicePixelRatio || 1\n\n      canvas.width = rect.width * dpr\n      canvas.height = rect.height * dpr\n      canvas.style.width = `${rect.width}px`\n      canvas.style.height = `${rect.height}px`\n\n      const ctx = canvas.getContext(\"2d\")\n      if (ctx) {\n        ctx.scale(dpr, dpr)\n      }\n    })\n\n    resizeObserver.observe(container)\n    return () => resizeObserver.disconnect()\n  }, [])\n\n  useEffect(() => {\n    if (!active) {\n      if (\n        mediaRecorderRef.current &&\n        mediaRecorderRef.current.state !== \"inactive\"\n      ) {\n        mediaRecorderRef.current.stop()\n      }\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n      }\n      // Process recorded audio when stopping\n      if (enableAudioPlayback && audioChunksRef.current.length > 0) {\n        const audioBlob = new Blob(audioChunksRef.current, {\n          type: \"audio/webm\",\n        })\n        processAudioBlob(audioBlob)\n      }\n      return\n    }\n\n    setDragOffset?.(0)\n    historyRef.current = []\n    audioChunksRef.current = []\n    audioBufferRef.current = null\n    setPlaybackPosition(null)\n\n    const setupMicrophone = async () => {\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({\n          audio: true,\n        })\n        streamRef.current = stream\n\n        const audioContext = new (window.AudioContext ||\n          (window as unknown as { webkitAudioContext: typeof AudioContext })\n            .webkitAudioContext)()\n        const analyser = audioContext.createAnalyser()\n        analyser.fftSize = fftSize\n        analyser.smoothingTimeConstant = smoothingTimeConstant\n\n        const source = audioContext.createMediaStreamSource(stream)\n        source.connect(analyser)\n\n        audioContextRef.current = audioContext\n        analyserRef.current = analyser\n\n        if (enableAudioPlayback) {\n          const mediaRecorder = new MediaRecorder(stream)\n          mediaRecorderRef.current = mediaRecorder\n\n          mediaRecorder.ondataavailable = (event) => {\n            if (event.data.size > 0) {\n              audioChunksRef.current.push(event.data)\n            }\n          }\n\n          mediaRecorder.start(100)\n        }\n      } catch (error) {\n        onError?.(error as Error)\n      }\n    }\n\n    setupMicrophone()\n\n    return () => {\n      if (\n        mediaRecorderRef.current &&\n        mediaRecorderRef.current.state !== \"inactive\"\n      ) {\n        mediaRecorderRef.current.stop()\n      }\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n      }\n      if (sourceNodeRef.current) {\n        sourceNodeRef.current.stop()\n      }\n      if (scrubSourceRef.current) {\n        scrubSourceRef.current.stop()\n      }\n    }\n  }, [\n    active,\n    fftSize,\n    smoothingTimeConstant,\n    onError,\n    setDragOffset,\n    enableAudioPlayback,\n    historyRef,\n  ])\n\n  const processAudioBlob = async (blob: Blob) => {\n    try {\n      const arrayBuffer = await blob.arrayBuffer()\n      if (audioContextRef.current) {\n        const audioBuffer =\n          await audioContextRef.current.decodeAudioData(arrayBuffer)\n        audioBufferRef.current = audioBuffer\n      }\n    } catch (error) {\n      console.error(\"Error processing audio:\", error)\n    }\n  }\n\n  const playScrubSound = useCallback(\n    (position: number, direction: number) => {\n      if (\n        !enableAudioPlayback ||\n        !audioBufferRef.current ||\n        !audioContextRef.current\n      )\n        return\n\n      if (scrubSourceRef.current) {\n        try {\n          scrubSourceRef.current.stop()\n        } catch {}\n      }\n\n      const source = audioContextRef.current.createBufferSource()\n      source.buffer = audioBufferRef.current\n\n      const speed = Math.abs(direction)\n      const playbackRate =\n        direction > 0\n          ? Math.min(3, 1 + speed * 0.1)\n          : Math.max(-3, -1 - speed * 0.1)\n\n      source.playbackRate.value = playbackRate\n\n      const filter = audioContextRef.current.createBiquadFilter()\n      filter.type = \"lowpass\"\n      filter.frequency.value = Math.max(200, 2000 - speed * 100)\n\n      source.connect(filter)\n      filter.connect(audioContextRef.current.destination)\n\n      const startTime = Math.max(\n        0,\n        Math.min(position, audioBufferRef.current.duration - 0.1)\n      )\n      source.start(0, startTime, 0.1)\n      scrubSourceRef.current = source\n    },\n    [enableAudioPlayback]\n  )\n\n  const playFromPosition = useCallback(\n    (position: number) => {\n      if (\n        !enableAudioPlayback ||\n        !audioBufferRef.current ||\n        !audioContextRef.current\n      )\n        return\n\n      if (sourceNodeRef.current) {\n        try {\n          sourceNodeRef.current.stop()\n        } catch {}\n      }\n\n      const source = audioContextRef.current.createBufferSource()\n      source.buffer = audioBufferRef.current\n      source.playbackRate.value = playbackRate\n      source.connect(audioContextRef.current.destination)\n\n      const startTime = Math.max(\n        0,\n        Math.min(position, audioBufferRef.current.duration)\n      )\n      source.start(0, startTime)\n      sourceNodeRef.current = source\n\n      playbackStartTimeRef.current =\n        audioContextRef.current.currentTime - startTime\n      setPlaybackPosition(startTime)\n\n      source.onended = () => {\n        setPlaybackPosition(null)\n      }\n    },\n    [enableAudioPlayback, playbackRate]\n  )\n\n  useEffect(() => {\n    if (playbackPosition === null || !audioBufferRef.current) return\n\n    let animationId: number\n    const updatePlaybackVisual = () => {\n      if (\n        audioContextRef.current &&\n        sourceNodeRef.current &&\n        audioBufferRef.current\n      ) {\n        const elapsed =\n          audioContextRef.current.currentTime - playbackStartTimeRef.current\n        const currentPos = playbackPosition + elapsed * playbackRate\n\n        if (currentPos < audioBufferRef.current.duration) {\n          const progressRatio = currentPos / audioBufferRef.current.duration\n          const currentBarIndex = Math.floor(\n            progressRatio * historyRef.current.length\n          )\n          const step = barWidth + barGap\n\n          const containerWidth =\n            containerRef.current?.getBoundingClientRect().width || 0\n          const viewBars = Math.floor(containerWidth / step)\n          const targetOffset =\n            -(currentBarIndex - (historyRef.current.length - viewBars)) * step\n          const clampedOffset = Math.max(\n            -(historyRef.current.length - viewBars) * step,\n            Math.min(0, targetOffset)\n          )\n\n          setDragOffset?.(clampedOffset)\n          animationId = requestAnimationFrame(updatePlaybackVisual)\n        } else {\n          setPlaybackPosition(null)\n          const step = barWidth + barGap\n          const containerWidth =\n            containerRef.current?.getBoundingClientRect().width || 0\n          const viewBars = Math.floor(containerWidth / step)\n          setDragOffset?.(-(historyRef.current.length - viewBars) * step)\n        }\n      }\n    }\n\n    animationId = requestAnimationFrame(updatePlaybackVisual)\n\n    return () => {\n      if (animationId) cancelAnimationFrame(animationId)\n    }\n  }, [\n    playbackPosition,\n    playbackRate,\n    barWidth,\n    baseBarHeight,\n    barGap,\n    setDragOffset,\n    historyRef,\n  ])\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n    if (!active && historyRef.current.length === 0 && playbackPosition === null)\n      return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    const animate = (currentTime: number) => {\n      if (active && currentTime - lastUpdateRef.current > updateRate) {\n        lastUpdateRef.current = currentTime\n\n        if (analyserRef.current) {\n          const dataArray = new Uint8Array(\n            analyserRef.current.frequencyBinCount\n          )\n          analyserRef.current.getByteFrequencyData(dataArray)\n\n          let sum = 0\n          for (let i = 0; i < dataArray.length; i++) {\n            sum += dataArray[i]\n          }\n          const average = (sum / dataArray.length / 255) * sensitivity\n\n          historyRef.current.push(Math.min(1, Math.max(0.05, average)))\n\n          if (historyRef.current.length > historySize) {\n            historyRef.current.shift()\n          }\n        }\n      }\n\n      const rect = canvas.getBoundingClientRect()\n      ctx.clearRect(0, 0, rect.width, rect.height)\n\n      const computedBarColor =\n        barColor ||\n        getComputedStyle(canvas).getPropertyValue(\"--foreground\") ||\n        \"#000\"\n\n      const step = barWidth + barGap\n      const barCount = Math.floor(rect.width / step)\n      const centerY = rect.height / 2\n\n      const dataToRender = historyRef.current\n\n      if (dataToRender.length > 0) {\n        const offsetInBars = Math.floor(dragOffset / step)\n\n        for (let i = 0; i < barCount; i++) {\n          let dataIndex\n\n          if (active) {\n            dataIndex = dataToRender.length - 1 - i\n          } else {\n            dataIndex = Math.max(\n              0,\n              Math.min(\n                dataToRender.length - 1,\n                dataToRender.length - 1 - i - Math.floor(offsetInBars)\n              )\n            )\n          }\n\n          if (dataIndex >= 0 && dataIndex < dataToRender.length) {\n            const value = dataToRender[dataIndex]\n            if (value !== undefined) {\n              const x = rect.width - (i + 1) * step\n              const barHeight = Math.max(\n                baseBarHeight,\n                value * rect.height * 0.7\n              )\n              const y = centerY - barHeight / 2\n\n              ctx.fillStyle = computedBarColor\n              ctx.globalAlpha = 0.3 + value * 0.7\n\n              if (barRadius > 0) {\n                ctx.beginPath()\n                ctx.roundRect(x, y, barWidth, barHeight, barRadius)\n                ctx.fill()\n              } else {\n                ctx.fillRect(x, y, barWidth, barHeight)\n              }\n            }\n          }\n        }\n      }\n\n      if (fadeEdges && fadeWidth > 0) {\n        const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)\n        const fadePercent = Math.min(0.2, fadeWidth / rect.width)\n\n        gradient.addColorStop(0, \"rgba(255,255,255,1)\")\n        gradient.addColorStop(fadePercent, \"rgba(255,255,255,0)\")\n        gradient.addColorStop(1 - fadePercent, \"rgba(255,255,255,0)\")\n        gradient.addColorStop(1, \"rgba(255,255,255,1)\")\n\n        ctx.globalCompositeOperation = \"destination-out\"\n        ctx.fillStyle = gradient\n        ctx.fillRect(0, 0, rect.width, rect.height)\n        ctx.globalCompositeOperation = \"source-over\"\n      }\n\n      ctx.globalAlpha = 1\n\n      animationRef.current = requestAnimationFrame(animate)\n    }\n\n    if (active || historyRef.current.length > 0 || playbackPosition !== null) {\n      animationRef.current = requestAnimationFrame(animate)\n    }\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n      }\n    }\n  }, [\n    active,\n    sensitivity,\n    updateRate,\n    historySize,\n    barWidth,\n    baseBarHeight,\n    barGap,\n    barRadius,\n    barColor,\n    fadeEdges,\n    fadeWidth,\n    dragOffset,\n    playbackPosition,\n    historyRef,\n  ])\n\n  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (active || historyRef.current.length === 0) return\n\n    e.preventDefault()\n    setIsDragging(true)\n    dragStartXRef.current = e.clientX\n    dragStartOffsetRef.current = dragOffset\n  }\n\n  useEffect(() => {\n    if (!isDragging) return\n\n    let lastScrubTime = 0\n    let lastMouseX = dragStartXRef.current\n    const handleMouseMove = (e: MouseEvent) => {\n      const deltaX = e.clientX - dragStartXRef.current\n      const newOffset = dragStartOffsetRef.current - deltaX * 0.5 // Reduce sensitivity\n\n      const step = barWidth + barGap\n      const maxBars = historyRef.current.length\n      const viewWidth = canvasRef.current?.getBoundingClientRect().width || 0\n      const viewBars = Math.floor(viewWidth / step)\n\n      const maxOffset = Math.max(0, (maxBars - viewBars) * step)\n      const minOffset = 0\n      const clampedOffset = Math.max(minOffset, Math.min(maxOffset, newOffset))\n\n      setDragOffset?.(clampedOffset)\n\n      const now = Date.now()\n      if (\n        enableAudioPlayback &&\n        audioBufferRef.current &&\n        now - lastScrubTime > 50\n      ) {\n        lastScrubTime = now\n        const offsetBars = Math.floor(clampedOffset / step)\n        const rightmostBarIndex = Math.max(\n          0,\n          Math.min(maxBars - 1, maxBars - 1 - offsetBars)\n        )\n        const audioPosition =\n          (rightmostBarIndex / maxBars) * audioBufferRef.current.duration\n        const direction = e.clientX - lastMouseX\n        lastMouseX = e.clientX\n        playScrubSound(\n          Math.max(\n            0,\n            Math.min(audioBufferRef.current.duration - 0.1, audioPosition)\n          ),\n          direction\n        )\n      }\n    }\n\n    const handleMouseUp = () => {\n      setIsDragging(false)\n\n      if (enableAudioPlayback && audioBufferRef.current) {\n        const step = barWidth + barGap\n        const maxBars = historyRef.current.length\n        const offsetBars = Math.floor(dragOffset / step)\n        const rightmostBarIndex = Math.max(\n          0,\n          Math.min(maxBars - 1, maxBars - 1 - offsetBars)\n        )\n        const audioPosition =\n          (rightmostBarIndex / maxBars) * audioBufferRef.current.duration\n        playFromPosition(\n          Math.max(\n            0,\n            Math.min(audioBufferRef.current.duration - 0.1, audioPosition)\n          )\n        )\n      }\n\n      if (scrubSourceRef.current) {\n        try {\n          scrubSourceRef.current.stop()\n        } catch {}\n      }\n    }\n\n    document.addEventListener(\"mousemove\", handleMouseMove)\n    document.addEventListener(\"mouseup\", handleMouseUp)\n\n    return () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove)\n      document.removeEventListener(\"mouseup\", handleMouseUp)\n    }\n  }, [\n    isDragging,\n    barWidth,\n    barGap,\n    setDragOffset,\n    dragOffset,\n    enableAudioPlayback,\n    playScrubSound,\n    playFromPosition,\n    historyRef,\n  ])\n\n  return (\n    <div\n      className={cn(\n        \"relative flex items-center\",\n        !active && historyRef.current.length > 0 && \"cursor-pointer\",\n        className\n      )}\n      onMouseDown={handleMouseDown}\n      ref={containerRef}\n      role={!active && historyRef.current.length > 0 ? \"slider\" : undefined}\n      aria-label={\n        !active && historyRef.current.length > 0\n          ? \"Drag to scrub through recording\"\n          : undefined\n      }\n      aria-valuenow={\n        !active && historyRef.current.length > 0\n          ? Math.abs(dragOffset)\n          : undefined\n      }\n      aria-valuemin={!active && historyRef.current.length > 0 ? 0 : undefined}\n      aria-valuemax={\n        !active && historyRef.current.length > 0\n          ? historyRef.current.length\n          : undefined\n      }\n      tabIndex={!active && historyRef.current.length > 0 ? 0 : undefined}\n      style={{ height: heightStyle }}\n      {...props}\n    >\n      <canvas className=\"block h-full w-full\" ref={canvasRef} />\n    </div>\n  )\n}\n\nexport type RecordingWaveformProps = Omit<\n  WaveformProps,\n  \"data\" | \"onBarClick\"\n> & {\n  recording?: boolean\n  fftSize?: number\n  smoothingTimeConstant?: number\n  sensitivity?: number\n  onError?: (error: Error) => void\n  onRecordingComplete?: (data: number[]) => void\n  updateRate?: number\n  showHandle?: boolean\n}\n\nexport const RecordingWaveform = ({\n  recording = false,\n  fftSize = 256,\n  smoothingTimeConstant = 0.8,\n  sensitivity = 1,\n  onError,\n  onRecordingComplete,\n  updateRate = 50,\n  showHandle = true,\n  barWidth = 3,\n  barHeight: baseBarHeight = 4,\n  barGap = 1,\n  barRadius = 1,\n  barColor,\n  height = 128,\n  className,\n  ...props\n}: RecordingWaveformProps) => {\n  const [recordedData, setRecordedData] = useState<number[]>([])\n  const [viewPosition, setViewPosition] = useState(1)\n  const [isDragging, setIsDragging] = useState(false)\n  const [isRecordingComplete, setIsRecordingComplete] = useState(false)\n\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const recordingDataRef = useRef<number[]>([])\n  const analyserRef = useRef<AnalyserNode | null>(null)\n  const audioContextRef = useRef<AudioContext | null>(null)\n  const streamRef = useRef<MediaStream | null>(null)\n  const animationRef = useRef<number>(0)\n  const lastUpdateRef = useRef<number>(0)\n\n  const heightStyle = typeof height === \"number\" ? `${height}px` : height\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const container = containerRef.current\n    if (!canvas || !container) return\n\n    const resizeObserver = new ResizeObserver(() => {\n      const rect = container.getBoundingClientRect()\n      const dpr = window.devicePixelRatio || 1\n\n      canvas.width = rect.width * dpr\n      canvas.height = rect.height * dpr\n      canvas.style.width = `${rect.width}px`\n      canvas.style.height = `${rect.height}px`\n\n      const ctx = canvas.getContext(\"2d\")\n      if (ctx) {\n        ctx.scale(dpr, dpr)\n      }\n    })\n\n    resizeObserver.observe(container)\n    return () => resizeObserver.disconnect()\n  }, [])\n\n  useEffect(() => {\n    if (!recording) {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n      }\n      if (\n        audioContextRef.current &&\n        audioContextRef.current.state !== \"closed\"\n      ) {\n        audioContextRef.current.close()\n      }\n\n      if (recordingDataRef.current.length > 0) {\n        setRecordedData([...recordingDataRef.current])\n        setIsRecordingComplete(true)\n        onRecordingComplete?.(recordingDataRef.current)\n      }\n      return\n    }\n\n    setIsRecordingComplete(false)\n    recordingDataRef.current = []\n    setRecordedData([])\n    setViewPosition(1)\n\n    const setupMicrophone = async () => {\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({\n          audio: true,\n        })\n        streamRef.current = stream\n\n        const audioContext = new (window.AudioContext ||\n          (window as unknown as { webkitAudioContext: typeof AudioContext })\n            .webkitAudioContext)()\n        const analyser = audioContext.createAnalyser()\n        analyser.fftSize = fftSize\n        analyser.smoothingTimeConstant = smoothingTimeConstant\n\n        const source = audioContext.createMediaStreamSource(stream)\n        source.connect(analyser)\n\n        audioContextRef.current = audioContext\n        analyserRef.current = analyser\n      } catch (error) {\n        onError?.(error as Error)\n      }\n    }\n\n    setupMicrophone()\n\n    return () => {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n      }\n      if (\n        audioContextRef.current &&\n        audioContextRef.current.state !== \"closed\"\n      ) {\n        audioContextRef.current.close()\n      }\n    }\n  }, [recording, fftSize, smoothingTimeConstant, onError, onRecordingComplete])\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    const animate = (currentTime: number) => {\n      if (recording && currentTime - lastUpdateRef.current > updateRate) {\n        lastUpdateRef.current = currentTime\n\n        if (analyserRef.current) {\n          const dataArray = new Uint8Array(\n            analyserRef.current.frequencyBinCount\n          )\n          analyserRef.current.getByteFrequencyData(dataArray)\n\n          let sum = 0\n          for (let i = 0; i < dataArray.length; i++) {\n            sum += dataArray[i]\n          }\n          const average = (sum / dataArray.length / 255) * sensitivity\n\n          recordingDataRef.current.push(Math.min(1, Math.max(0.05, average)))\n        }\n      }\n\n      const rect = canvas.getBoundingClientRect()\n      ctx.clearRect(0, 0, rect.width, rect.height)\n\n      const computedBarColor =\n        barColor ||\n        getComputedStyle(canvas).getPropertyValue(\"--foreground\") ||\n        \"#000\"\n\n      const dataToRender = recording ? recordingDataRef.current : recordedData\n\n      if (dataToRender.length > 0) {\n        const step = barWidth + barGap\n        const barsVisible = Math.floor(rect.width / step)\n        const centerY = rect.height / 2\n\n        let startIndex = 0\n        if (!recording && isRecordingComplete) {\n          const totalBars = dataToRender.length\n          if (totalBars > barsVisible) {\n            startIndex = Math.floor((totalBars - barsVisible) * viewPosition)\n          }\n        } else if (recording) {\n          startIndex = Math.max(0, dataToRender.length - barsVisible)\n        }\n\n        for (\n          let i = 0;\n          i < barsVisible && startIndex + i < dataToRender.length;\n          i++\n        ) {\n          const value = dataToRender[startIndex + i] || 0.1\n          const x = i * step\n          const barHeight = Math.max(baseBarHeight, value * rect.height * 0.7)\n          const y = centerY - barHeight / 2\n\n          ctx.fillStyle = computedBarColor\n          ctx.globalAlpha = 0.3 + value * 0.7\n\n          if (barRadius > 0) {\n            ctx.beginPath()\n            ctx.roundRect(x, y, barWidth, barHeight, barRadius)\n            ctx.fill()\n          } else {\n            ctx.fillRect(x, y, barWidth, barHeight)\n          }\n        }\n\n        if (!recording && isRecordingComplete && showHandle) {\n          const indicatorX = rect.width * viewPosition\n\n          ctx.strokeStyle = computedBarColor\n          ctx.globalAlpha = 0.5\n          ctx.lineWidth = 2\n          ctx.beginPath()\n          ctx.moveTo(indicatorX, 0)\n          ctx.lineTo(indicatorX, rect.height)\n          ctx.stroke()\n          ctx.fillStyle = computedBarColor\n          ctx.globalAlpha = 1\n          ctx.beginPath()\n          ctx.arc(indicatorX, centerY, 6, 0, Math.PI * 2)\n          ctx.fill()\n        }\n      }\n\n      ctx.globalAlpha = 1\n\n      animationRef.current = requestAnimationFrame(animate)\n    }\n\n    animationRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n      }\n    }\n  }, [\n    recording,\n    recordedData,\n    viewPosition,\n    isRecordingComplete,\n    sensitivity,\n    updateRate,\n    showHandle,\n    barWidth,\n    baseBarHeight,\n    barGap,\n    barRadius,\n    barColor,\n  ])\n\n  const handleScrub = useCallback(\n    (clientX: number) => {\n      const container = containerRef.current\n      if (!container || recording || !isRecordingComplete) return\n\n      const rect = container.getBoundingClientRect()\n      const x = Math.max(0, Math.min(clientX - rect.left, rect.width))\n      const position = x / rect.width\n\n      setViewPosition(position)\n    },\n    [recording, isRecordingComplete]\n  )\n\n  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (recording || !isRecordingComplete) return\n\n    e.preventDefault()\n    setIsDragging(true)\n    handleScrub(e.clientX)\n  }\n\n  useEffect(() => {\n    if (!isDragging) return\n\n    const handleMouseMove = (e: MouseEvent) => {\n      handleScrub(e.clientX)\n    }\n\n    const handleMouseUp = () => {\n      setIsDragging(false)\n    }\n\n    document.addEventListener(\"mousemove\", handleMouseMove)\n    document.addEventListener(\"mouseup\", handleMouseUp)\n\n    return () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove)\n      document.removeEventListener(\"mouseup\", handleMouseUp)\n    }\n  }, [isDragging, handleScrub])\n\n  return (\n    <div\n      aria-label={\n        isRecordingComplete && !recording\n          ? \"Drag to scrub through recording\"\n          : undefined\n      }\n      aria-valuenow={\n        isRecordingComplete && !recording ? viewPosition * 100 : undefined\n      }\n      aria-valuemin={isRecordingComplete && !recording ? 0 : undefined}\n      aria-valuemax={isRecordingComplete && !recording ? 100 : undefined}\n      className={cn(\n        \"relative flex items-center\",\n        isRecordingComplete && !recording && \"cursor-pointer\",\n        className\n      )}\n      onMouseDown={handleMouseDown}\n      ref={containerRef}\n      role={isRecordingComplete && !recording ? \"slider\" : undefined}\n      style={{ height: heightStyle }}\n      tabIndex={isRecordingComplete && !recording ? 0 : undefined}\n      {...props}\n    >\n      <canvas className=\"block h-full w-full\" ref={canvasRef} />\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}