{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-oscilloscope",
  "type": "registry:ui",
  "title": "Oscilloscope",
  "description": "A time-domain waveform trace with min/max downsampling so transients survive, and optional zero-crossing trigger so a steady tone stands still instead of sliding.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/ui/oscilloscope.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/oscilloscope\n * Copyright © [Maru](https://maru.audio)\n *\n * Maru UI Kit — https://maru.audio/ui-kit\n *\n * Installed from the Maru UI registry. This file is yours now: edit it\n * freely, it will not be overwritten unless you re-run the add command.\n *\n * Keep this item’s companion files and CSS imports together. Dependencies\n * declared by the registry are installed with the source.\n */\n/**\n * Oscilloscope — a time-domain waveform trace.\n *\n * Feed it an `AnalyserNode` or a `getData` callback and it draws the current\n * frame at animation rate. With `trigger` on it aligns each frame to a rising\n * zero crossing, which is what stops a steady tone from sliding across the\n * display; without it you see the raw buffer, drift and all.\n *\n * Styling comes from CSS custom properties — see `lib/theme.ts`.\n */\n\nimport { useCallback, useRef } from \"react\"\nimport type { CSSProperties } from \"react\"\nimport {\n  canvasColor,\n  token,\n} from \"maruaudio/ui/shared/theme\"\nimport {\n  useAudioCanvas,\n  type AudioSourceProps,\n} from \"maruaudio/ui/shared/use-audio-source\"\n\nexport interface OscilloscopeProps extends AudioSourceProps {\n  width?: number | string\n  height?: number\n  /** Vertical zoom. 1 means a full-scale signal fills the display. */\n  gain?: number\n  /**\n   * Align each frame to a rising zero crossing so a steady tone stands still\n   * instead of sliding. Costs one scan of the buffer per frame.\n   */\n  trigger?: boolean\n  /** Draw the centre line and grid. */\n  showGrid?: boolean\n  /** Trace colour. Defaults to the accent token. */\n  color?: string\n  /** Trace thickness in CSS pixels. */\n  lineWidth?: number\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nexport function Oscilloscope({\n  analyser,\n  getData,\n  dataSize = 2048,\n  paused = false,\n  width = \"100%\",\n  height = 140,\n  gain = 1,\n  trigger = true,\n  showGrid = true,\n  color,\n  lineWidth = 1.5,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: OscilloscopeProps) {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null)\n\n  const draw = useCallback(\n    ({\n      ctx,\n      width: w,\n      height: h,\n      data,\n    }: {\n      ctx: CanvasRenderingContext2D\n      width: number\n      height: number\n      data: Float32Array | null\n    }) => {\n      const styles = getComputedStyle(ctx.canvas)\n      const stroke = canvasColor(ctx.canvas, color ?? token.accent)\n      const grid = styles.getPropertyValue(\"--uikit-border\").trim() || \"rgba(148,163,184,0.35)\"\n\n      ctx.clearRect(0, 0, w, h)\n\n      if (showGrid) {\n        ctx.strokeStyle = grid\n        ctx.globalAlpha = 0.5\n        ctx.lineWidth = 1\n        ctx.beginPath()\n        ctx.moveTo(0, h / 2)\n        ctx.lineTo(w, h / 2)\n        ctx.stroke()\n        ctx.globalAlpha = 0.22\n        for (let i = 1; i < 4; i += 1) {\n          const x = (w * i) / 4\n          ctx.beginPath()\n          ctx.moveTo(x, 0)\n          ctx.lineTo(x, h)\n          ctx.stroke()\n        }\n        ctx.globalAlpha = 1\n      }\n\n      if (!data || data.length === 0) return\n\n      // Trigger: find the first rising zero crossing in the first half of the\n      // buffer, and start drawing there. Searching only half leaves a full\n      // half-buffer of samples to actually plot.\n      let start = 0\n      if (trigger) {\n        const limit = Math.floor(data.length / 2)\n        for (let i = 1; i < limit; i += 1) {\n          if (data[i - 1] <= 0 && data[i] > 0) {\n            start = i\n            break\n          }\n        }\n      }\n\n      const usable = data.length - start\n      const mid = h / 2\n      const scale = (h / 2) * gain\n\n      ctx.strokeStyle = stroke\n      ctx.lineWidth = lineWidth\n      ctx.lineJoin = \"round\"\n      ctx.beginPath()\n\n      // One column per pixel: with more samples than pixels, plot the min/max\n      // of each column's samples so transients survive the downsample instead\n      // of being skipped over by naive sampling.\n      const samplesPerColumn = usable / w\n      if (samplesPerColumn > 2) {\n        for (let x = 0; x < w; x += 1) {\n          const from = start + Math.floor(x * samplesPerColumn)\n          const to = Math.min(data.length, start + Math.floor((x + 1) * samplesPerColumn))\n          let min = 1\n          let max = -1\n          for (let i = from; i < to; i += 1) {\n            if (data[i] < min) min = data[i]\n            if (data[i] > max) max = data[i]\n          }\n          if (min > max) continue\n          ctx.moveTo(x + 0.5, mid - max * scale)\n          ctx.lineTo(x + 0.5, mid - min * scale)\n        }\n      } else {\n        for (let x = 0; x < w; x += 1) {\n          const index = start + Math.floor(x * samplesPerColumn)\n          const sample = data[Math.min(index, data.length - 1)]\n          const y = mid - sample * scale\n          if (x === 0) ctx.moveTo(x, y)\n          else ctx.lineTo(x, y)\n        }\n      }\n\n      ctx.stroke()\n    },\n    [color, showGrid, trigger, gain, lineWidth],\n  )\n\n  useAudioCanvas(canvasRef, {\n    analyser,\n    getData,\n    dataSize,\n    paused,\n    kind: \"time\",\n    draw,\n  })\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={`uikit-control${className ? ` ${className}` : \"\"}`}\n      role=\"img\"\n      aria-label={ariaLabel ?? \"Oscilloscope\"}\n      style={{\n        display: \"block\",\n        width,\n        height,\n        background: token.surface,\n        borderRadius: token.radius,\n        border: `1px solid ${token.border}`,\n        ...style,\n      }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/oscilloscope.tsx"
    }
  ]
}
