{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "electric-waveform",
  "title": "Electric Waveform",
  "description": "An electric wave or oscilloscope trace with forks and sparks.",
  "dependencies": [
    "solid-js@^1.9.15"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/electric",
    "@audiocn-solid/use-frame-source",
    "@audiocn-solid/use-reduced-motion",
    "@audiocn-solid/use-visibility"
  ],
  "files": [
    {
      "path": "components/ui/electric-waveform.tsx",
      "content": "import { useFrameSource } from \"@/hooks/use-frame-source\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibility } from \"@/hooks/use-visibility\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport { subscribeFrame } from \"@/lib/audio/frame-loop\";\nimport type { FrameSource, VisualFrame } from \"@/lib/audio/types\";\nimport { WAVE_LINE_MAX_POINTS, createWaveLine } from \"@/lib/audio/wave-line\";\nimport type { WaveLineMode } from \"@/lib/audio/wave-line\";\nimport {\n  ELECTRIC_CANVAS_CLASS,\n  ELECTRIC_GLOW_CLASS,\n  clearElectricCanvas,\n  createElectricSparks,\n  createRandom,\n  displace,\n  emitSpark,\n  fitElectricCanvases,\n  moveSparks,\n  readElectricColors,\n  strokeSparks,\n} from \"@/lib/electric\";\nimport type {\n  ElectricCanvasSize,\n  ElectricColors,\n  ElectricSparks,\n} from \"@/lib/electric\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type { DivDOMProps } from \"@/lib/solid/jsx-types\";\nimport { omitProps } from \"@/lib/solid/props\";\nimport { setRefValue } from \"@/lib/solid/ref\";\nimport type { RefTarget } from \"@/lib/solid/ref\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_INTENSITY = 0.6;\n\nconst DEFAULT_LINE_WIDTH = 3;\n\nconst REDUCED_MOTION_INTERVAL_MS = 250;\n\nconst COLOR_REFRESH_FRAMES = 30;\n\nconst MS_PER_SECOND = 1000;\n\nconst FRAME_MS = 16.67;\n\nconst MAX_STEP_SECONDS = 0.05;\n\n/** Width of the faded ends, in pixels. */\nconst FADE_PX = 24;\n\n/** One point every this many pixels along the line. */\nconst POINT_SPACING_PX = 4;\n\n/** Crackle is built in short chunks of 2^2 segments that share their ends. */\nconst CHUNK = 2 ** 2;\n\nconst MIN_POINTS = CHUNK * 8 + 1;\n\nconst MAX_POINTS = WAVE_LINE_MAX_POINTS;\n\n/** How much each finer split of the crackle bends compared with the one before. */\nconst ROUGHNESS = 0.8;\n\n/** Each jagged shape holds at least this long, so the crackle never strobes. */\nconst JITTER_MS = 42;\n\n/** Brightness flicker, at most 12%. */\nconst FLICKER = 0.12;\n\n/** Crackle at rest, in pixels at full intensity. */\nconst HUM_PX = 1.5;\n\n/** Extra crackle at full level, in pixels at full intensity. */\nconst CRACKLE_PX = 6;\n\nconst MAX_BRANCHES = 3;\n\nconst BRANCH_POINTS = 2 ** 3 + 1;\n\n/** A point must be this far from the middle to throw a fork. */\nconst BRANCH_THRESHOLD = 0.3;\n\nconst BRANCH_CHANCE = 0.6;\n\nconst BRANCH_CANDIDATES = 3;\n\nconst BRANCH_MIN_PX = 10;\n\nconst BRANCH_MAX_PX = 30;\n\nconst BRANCH_MIN_MS = 60;\n\nconst BRANCH_MAX_MS = 140;\n\n/** How far a fork bends, relative to its length. */\nconst BRANCH_BOW = 0.35;\n\nconst MAX_SPARKS = 48;\n\n/** A rise in loudness this big in one frame throws sparks. */\nconst SPARK_RISE = 0.1;\n\nconst SPARK_MIN_LEVEL = 0.3;\n\nconst SPARK_COOLDOWN_MS = 90;\n\n/** Pixels per second squared. */\nconst SPARK_GRAVITY = 520;\n\n/** Pixels per second. */\nconst SPARK_SPEED = 170;\n\n/** Pixels per second, sideways. */\nconst SPARK_SPREAD = 90;\n\nconst SPARK_MIN_MS = 260;\n\nconst SPARK_MAX_MS = 560;\n\nconst SPARK_TRAIL_SECONDS = 0.03;\n\nexport type ElectricWaveformMode = WaveLineMode;\n\nexport interface ElectricWaveformActions {\n  /** Paint a frame directly. */\n  paint: (frame: VisualFrame) => void;\n  /** Forget the last frame, so the line falls flat. */\n  clear: () => void;\n}\n\nexport interface ElectricWaveformProps extends Omit<DivDOMProps, \"ref\"> {\n  source?: FrameSource<VisualFrame> | null;\n  /**\n   * `wave` draws a smooth wave shaped by the frequency bands. `scope` draws\n   * the signal itself, like an oscilloscope. Default `wave`.\n   */\n  mode?: ElectricWaveformMode;\n  /** Runs a pulse along the line, for connecting or thinking states. Default false. */\n  loading?: boolean;\n  /** How jagged and restless the line is, 0..1. Default 0.6. */\n  intensity?: number;\n  /** Forks of lightning branch off the peaks. Default true. */\n  arcs?: boolean;\n  /** Sparks fly off the peaks on sudden rises. Default true. */\n  sparks?: boolean;\n  /** Visual gain. Default 1. */\n  sensitivity?: number;\n  /** Line width in pixels. Default 3. */\n  lineWidth?: number;\n  /** Fade the left and right ends. Default true. */\n  fadeEdges?: boolean;\n  actionsRef?: RefTarget<ElectricWaveformActions>;\n  ref?: RefTarget<HTMLDivElement>;\n  className?: string;\n}\n\n/** A fork of lightning off the line. */\nexport interface ElectricBranch {\n  /** The point on the line it leaves from. */\n  point: number;\n  /** 1 up, −1 down: away from the middle. */\n  direction: number;\n  /** Pixels. */\n  length: number;\n  /** Sideways drift of the far end, relative to the length. */\n  lean: number;\n  bornMs: number;\n  /** 0 when the fork is gone. */\n  lifeMs: number;\n  /** Bend at each point, in units of the fork's bow. */\n  jitter: Float32Array;\n}\n\nexport interface ElectricTraceGeometry {\n  /** CSS pixels. */\n  width: number;\n  height: number;\n  lineWidth: number;\n}\n\nexport interface ElectricTraceOptions {\n  mode: ElectricWaveformMode;\n  loading: boolean;\n  intensity: number;\n  arcs: boolean;\n  sparks: boolean;\n  sensitivity: number;\n  /** A still line: no crackle, forks, sparks or drift. */\n  reducedMotion: boolean;\n  /** Makes the crackle repeatable. Each trace gets its own by default. */\n  seed?: number;\n}\n\nexport interface ElectricTrace {\n  /** Points in use along the width. */\n  count: number;\n  /** Height of the line at each point, −1..1, up positive. */\n  heights: Float32Array;\n  /** Crackle at each point, in units of its amplitude. */\n  crackle: Float32Array;\n  /** How loud the line is, 0..1. */\n  loudness: number;\n  /** Brightness, 0.88..1. */\n  flicker: number;\n  branches: ElectricBranch[];\n  sparks: ElectricSparks;\n  /** Advances to `nowMs` with the latest frame. Returns true when there is a signal. */\n  step: (\n    nowMs: number,\n    frame: VisualFrame | null,\n    geometry: ElectricTraceGeometry\n  ) => boolean;\n}\n\n/** Points for a width: one every few pixels, in whole crackle chunks. */\nconst pointCountFor = (width: number) =>\n  clamp(\n    Math.round(width / POINT_SPACING_PX / CHUNK) * CHUNK + 1,\n    MIN_POINTS,\n    MAX_POINTS\n  );\n\nconst middleOf = (geometry: ElectricTraceGeometry) => geometry.height / 2;\n\n/** Pixels from the middle to a height of 1, leaving room for the line. */\nconst reachOf = (geometry: ElectricTraceGeometry) =>\n  Math.max(0, geometry.height / 2 - Math.max(geometry.lineWidth * 2, 6));\n\nconst xOf = (geometry: ElectricTraceGeometry, count: number, point: number) =>\n  (point / (count - 1)) * geometry.width;\n\n/** Crackle in pixels at a point: louder lines and higher peaks crackle more. */\nconst crackleAt = (trace: ElectricTrace, intensity: number, point: number) =>\n  intensity *\n  (HUM_PX +\n    CRACKLE_PX *\n      (0.4 * trace.loudness + 0.6 * Math.abs(trace.heights[point] ?? 0)));\n\n/** Where a point of the line is drawn, in pixels down from the top. */\nconst yOf = (\n  trace: ElectricTrace,\n  geometry: ElectricTraceGeometry,\n  intensity: number,\n  point: number,\n  scale = 1,\n  crackleShare = 1\n) =>\n  middleOf(geometry) -\n  (trace.heights[point] ?? 0) * scale * reachOf(geometry) -\n  (trace.crackle[point] ?? 0) *\n    crackleAt(trace, intensity, point) *\n    crackleShare;\n\nlet traceCount = 0;\n\n/**\n * The moving parts of the electric line, apart from any canvas: its shape,\n * crackle, forks and sparks. With reduced motion it is a still, smooth line.\n */\nexport const createElectricTrace = ({\n  arcs,\n  intensity,\n  loading,\n  mode,\n  reducedMotion,\n  seed,\n  sensitivity,\n  sparks,\n}: ElectricTraceOptions): ElectricTrace => {\n  traceCount += 1;\n  const random = createRandom(seed ?? traceCount);\n  const signed = () => random() * 2 - 1;\n  const line = createWaveLine({ loading, mode, reducedMotion, sensitivity });\n  let lastMs = 0;\n  let lastJitterMs = -Infinity;\n  let lastSparkMs = -Infinity;\n  let lastStepSeconds = 0;\n  let previousPeak = 0;\n  let primed = false;\n\n  const trace: ElectricTrace = {\n    branches: Array.from({ length: MAX_BRANCHES }, () => ({\n      bornMs: 0,\n      direction: 1,\n      jitter: new Float32Array(BRANCH_POINTS),\n      lean: 0,\n      length: 0,\n      lifeMs: 0,\n      point: 0,\n    })),\n    count: MIN_POINTS,\n    crackle: new Float32Array(MAX_POINTS),\n    flicker: 1,\n    heights: line.heights,\n    loudness: 0,\n    sparks: createElectricSparks(MAX_SPARKS),\n    step: () => false,\n  };\n\n  const rerollCrackle = () => {\n    trace.crackle[0] = signed() * 0.5;\n\n    for (let start = 0; start + CHUNK < trace.count; start += CHUNK) {\n      displace(\n        trace.crackle,\n        start,\n        CHUNK + 1,\n        [trace.crackle[start] ?? 0, signed() * 0.5],\n        random,\n        ROUGHNESS\n      );\n    }\n\n    trace.flicker = 1 - FLICKER * random();\n  };\n\n  /** Picks the highest of a few random points, so forks favour the peaks. */\n  const pickPeak = () => {\n    let best = Math.floor(random() * trace.count);\n\n    for (let candidate = 1; candidate < BRANCH_CANDIDATES; candidate += 1) {\n      const point = Math.floor(random() * trace.count);\n\n      if (\n        Math.abs(trace.heights[point] ?? 0) > Math.abs(trace.heights[best] ?? 0)\n      ) {\n        best = point;\n      }\n    }\n\n    return best;\n  };\n\n  const updateBranches = (nowMs: number) => {\n    const point = pickPeak();\n    const height = trace.heights[point] ?? 0;\n\n    const charged =\n      Math.abs(height) >= BRANCH_THRESHOLD &&\n      random() < BRANCH_CHANCE * intensity * Math.abs(height);\n\n    const free = trace.branches.find((branch) => branch.lifeMs === 0);\n\n    if (charged && free) {\n      free.point = point;\n      free.direction = height >= 0 ? 1 : -1;\n      free.length =\n        (BRANCH_MIN_PX + random() * (BRANCH_MAX_PX - BRANCH_MIN_PX)) *\n        (0.6 + 0.8 * intensity);\n      free.lean = signed() * 0.6;\n      free.bornMs = nowMs;\n      free.lifeMs = BRANCH_MIN_MS + random() * (BRANCH_MAX_MS - BRANCH_MIN_MS);\n    }\n\n    for (const branch of trace.branches) {\n      if (branch.lifeMs > 0) {\n        displace(\n          branch.jitter,\n          0,\n          BRANCH_POINTS,\n          [0, signed() * 0.5],\n          random,\n          ROUGHNESS\n        );\n      }\n    }\n  };\n\n  const expireBranches = (nowMs: number) => {\n    for (const branch of trace.branches) {\n      if (branch.lifeMs > 0 && nowMs - branch.bornMs >= branch.lifeMs) {\n        branch.lifeMs = 0;\n      }\n    }\n  };\n\n  const throwSparks = (nowMs: number, geometry: ElectricTraceGeometry) => {\n    const rise = line.peak - previousPeak;\n    const rested = nowMs - lastSparkMs >= SPARK_COOLDOWN_MS;\n\n    if (rise < SPARK_RISE || line.peak < SPARK_MIN_LEVEL || !rested) {\n      return;\n    }\n\n    lastSparkMs = nowMs;\n    let peak = 0;\n\n    for (let point = 1; point < trace.count; point += 1) {\n      if (\n        Math.abs(trace.heights[point] ?? 0) > Math.abs(trace.heights[peak] ?? 0)\n      ) {\n        peak = point;\n      }\n    }\n\n    const upward = (trace.heights[peak] ?? 0) >= 0 ? -1 : 1;\n    const x = xOf(geometry, trace.count, peak);\n    const y = yOf(trace, geometry, intensity, peak);\n    const count = 3 + Math.floor(random() * 3);\n\n    for (let spark = 0; spark < count; spark += 1) {\n      emitSpark(trace.sparks, {\n        lifeMs: SPARK_MIN_MS + random() * (SPARK_MAX_MS - SPARK_MIN_MS),\n        vx: signed() * SPARK_SPREAD,\n        vy: upward * SPARK_SPEED * (0.5 + random()) * (0.5 + intensity),\n        x,\n        y,\n      });\n    }\n  };\n\n  const animate = (nowMs: number, geometry: ElectricTraceGeometry) => {\n    if (nowMs - lastJitterMs >= JITTER_MS) {\n      lastJitterMs = nowMs;\n      rerollCrackle();\n\n      if (arcs) {\n        updateBranches(nowMs);\n      }\n    }\n\n    expireBranches(nowMs);\n\n    if (sparks) {\n      if (primed) {\n        throwSparks(nowMs, geometry);\n      }\n\n      moveSparks(trace.sparks, lastStepSeconds, SPARK_GRAVITY);\n    }\n  };\n\n  trace.step = (nowMs, frame, geometry) => {\n    const elapsedMs = lastMs === 0 ? FRAME_MS : nowMs - lastMs;\n    lastMs = nowMs;\n    lastStepSeconds = clamp(elapsedMs / MS_PER_SECOND, 0, MAX_STEP_SECONDS);\n    const active = line.step(nowMs, frame, pointCountFor(geometry.width));\n    trace.count = line.count;\n    trace.loudness = line.loudness;\n\n    if (!reducedMotion) {\n      animate(nowMs, geometry);\n    }\n\n    previousPeak = line.peak;\n    primed = true;\n\n    return active;\n  };\n\n  return trace;\n};\n\ninterface PaintInput {\n  trace: ElectricTrace;\n  geometry: ElectricTraceGeometry;\n  colors: ElectricColors;\n  intensity: number;\n  nowMs: number;\n}\n\nconst traceLine = (\n  context: CanvasRenderingContext2D,\n  { geometry, intensity, trace }: PaintInput,\n  scale: number,\n  crackleShare: number\n) => {\n  context.beginPath();\n\n  for (let point = 0; point < trace.count; point += 1) {\n    const x = xOf(geometry, trace.count, point);\n    const y = yOf(trace, geometry, intensity, point, scale, crackleShare);\n\n    if (point === 0) {\n      context.moveTo(x, y);\n    } else {\n      context.lineTo(x, y);\n    }\n  }\n};\n\n/** The band between two scaled copies of the line, for the tall glow. */\nconst traceRibbon = (\n  context: CanvasRenderingContext2D,\n  { geometry, intensity, trace }: PaintInput,\n  inner: number,\n  outer: number\n) => {\n  context.beginPath();\n\n  for (let point = 0; point < trace.count; point += 1) {\n    const x = xOf(geometry, trace.count, point);\n    const y = yOf(trace, geometry, intensity, point, outer, 0.35);\n\n    if (point === 0) {\n      context.moveTo(x, y);\n    } else {\n      context.lineTo(x, y);\n    }\n  }\n\n  for (let point = trace.count - 1; point >= 0; point -= 1) {\n    const x = xOf(geometry, trace.count, point);\n    context.lineTo(x, yOf(trace, geometry, intensity, point, inner, 0.35));\n  }\n\n  context.closePath();\n};\n\nconst traceBranch = (\n  context: CanvasRenderingContext2D,\n  { geometry, intensity, trace }: PaintInput,\n  branch: ElectricBranch\n) => {\n  const point = Math.min(branch.point, trace.count - 1);\n  const x = xOf(geometry, trace.count, point);\n  const y = yOf(trace, geometry, intensity, point);\n  const dx = branch.lean * branch.length;\n  const dy = -branch.direction * branch.length;\n  const length = Math.hypot(dx, dy) || 1;\n  const bow = length * BRANCH_BOW;\n  context.beginPath();\n\n  for (let index = 0; index < BRANCH_POINTS; index += 1) {\n    const progress = index / (BRANCH_POINTS - 1);\n    const bend = (branch.jitter[index] ?? 0) * bow;\n    const px = x + dx * progress - (dy / length) * bend;\n    const py = y + dy * progress + (dx / length) * bend;\n\n    if (index === 0) {\n      context.moveTo(px, py);\n    } else {\n      context.lineTo(px, py);\n    }\n  }\n};\n\nconst strokeBranches = (\n  context: CanvasRenderingContext2D,\n  input: PaintInput,\n  lineWidth: number,\n  alpha: number\n) => {\n  context.lineWidth = lineWidth;\n\n  for (const branch of input.trace.branches) {\n    if (branch.lifeMs > 0) {\n      const age = (input.nowMs - branch.bornMs) / branch.lifeMs;\n      context.globalAlpha = clamp(alpha * (1 - age * age), 0, 1);\n      traceBranch(context, input, branch);\n      context.stroke();\n    }\n  }\n};\n\n/** Cuts the ends away with a gradient, so the line fades in and out. */\nconst fadeEnds = (\n  context: CanvasRenderingContext2D,\n  geometry: ElectricTraceGeometry\n) => {\n  const edge = Math.min(FADE_PX, geometry.width / 2);\n  context.globalAlpha = 1;\n  context.globalCompositeOperation = \"destination-out\";\n  const left = context.createLinearGradient(0, 0, edge, 0);\n  left.addColorStop(0, \"rgba(0, 0, 0, 1)\");\n  left.addColorStop(1, \"rgba(0, 0, 0, 0)\");\n  context.fillStyle = left;\n  context.fillRect(0, 0, edge, geometry.height);\n\n  const right = context.createLinearGradient(\n    geometry.width - edge,\n    0,\n    geometry.width,\n    0\n  );\n\n  right.addColorStop(0, \"rgba(0, 0, 0, 0)\");\n  right.addColorStop(1, \"rgba(0, 0, 0, 1)\");\n  context.fillStyle = right;\n  context.fillRect(geometry.width - edge, 0, edge, geometry.height);\n  context.globalCompositeOperation = \"source-over\";\n};\n\n/**\n * The tall, soft glow: bands that echo the wave above and below it, and a\n * wide stroke. The canvas is blurred with CSS, far cheaper than `shadowBlur`.\n */\nconst paintGlow = (context: CanvasRenderingContext2D, input: PaintInput) => {\n  const { geometry, trace } = input;\n  const strength = (0.4 + 0.6 * trace.loudness) * trace.flicker;\n  context.fillStyle = input.colors.glow;\n  context.strokeStyle = input.colors.glow;\n  context.globalAlpha = 0.22 * strength;\n  traceRibbon(context, input, 0.2, 2.6);\n  context.fill();\n  context.globalAlpha = 0.4 * strength;\n  traceRibbon(context, input, 0.6, 1.6);\n  context.fill();\n  context.globalAlpha = 0.6 * strength;\n  context.lineWidth = geometry.lineWidth * 4;\n  traceLine(context, input, 1, 0.6);\n  context.stroke();\n  strokeBranches(context, input, geometry.lineWidth * 1.5, 0.7);\n};\n\nconst paintMain = (context: CanvasRenderingContext2D, input: PaintInput) => {\n  const { geometry, trace } = input;\n  context.strokeStyle = input.colors.body;\n  context.globalAlpha = 0.9 * trace.flicker;\n  context.lineWidth = geometry.lineWidth;\n  traceLine(context, input, 1, 0.6);\n  context.stroke();\n  context.lineJoin = \"miter\";\n  strokeBranches(context, input, Math.max(1.5, geometry.lineWidth * 0.5), 0.7);\n  context.strokeStyle = input.colors.core;\n  context.globalAlpha = trace.flicker;\n  context.lineWidth = Math.max(1.5, geometry.lineWidth * 0.5);\n  traceLine(context, input, 1, 1);\n  context.stroke();\n  strokeBranches(context, input, 1, 1);\n  strokeSparks(\n    context,\n    trace.sparks,\n    Math.max(1, geometry.lineWidth * 0.4),\n    SPARK_TRAIL_SECONDS\n  );\n};\n\nexport const ElectricWaveform = (props: ElectricWaveformProps) => {\n  const reducedMotion = useReducedMotion();\n  let root: HTMLDivElement | undefined;\n  let main: HTMLCanvasElement | undefined;\n  let glow: HTMLCanvasElement | undefined;\n  let frame: VisualFrame | null = null;\n  const visible = useVisibility(() => root ?? null);\n\n  const paint = (next: VisualFrame) => {\n    frame = next;\n  };\n\n  useFrameSource(() => props.source, paint);\n  createCompatEffect(\n    () => props.actionsRef,\n    (ref) => {\n      setRefValue(ref, {\n        paint,\n        clear: () => {\n          frame = null;\n        },\n      });\n\n      return () => setRefValue(ref, null);\n    }\n  );\n  createCompatEffect(\n    () => ({\n      arcs: props.arcs ?? true,\n      intensity: props.intensity ?? DEFAULT_INTENSITY,\n      loading: props.loading ?? false,\n      mode: props.mode ?? \"wave\",\n      reducedMotion: reducedMotion(),\n      sensitivity: props.sensitivity ?? 1,\n      sparks: props.sparks ?? true,\n    }),\n    (options) => {\n      const container = root;\n      const sharp = main;\n      const blurred = glow;\n      const mainContext = sharp?.getContext(\"2d\");\n      const glowContext = blurred?.getContext(\"2d\");\n\n      if (!(container && sharp && blurred && mainContext && glowContext))\n        return;\n      const strength = clamp(options.intensity, 0, 1);\n      const trace = createElectricTrace({ ...options, intensity: strength });\n\n      let size: ElectricCanvasSize = {\n        glowRatio: 1,\n        height: 0,\n        ratio: 1,\n        width: 0,\n      };\n\n      let colors = readElectricColors(sharp, blurred);\n      let sinceColor = 0;\n      let lastPaint = 0;\n      let active = false;\n\n      const resize = () => {\n        size = fitElectricCanvases(sharp, blurred);\n      };\n\n      resize();\n      const observer = new ResizeObserver(resize);\n      observer.observe(sharp);\n\n      const unsubscribe = subscribeFrame((nowMs) => {\n        if (!visible.current || size.width === 0) return;\n\n        if (++sinceColor >= COLOR_REFRESH_FRAMES) {\n          sinceColor = 0;\n          colors = readElectricColors(sharp, blurred);\n        }\n\n        if (\n          options.reducedMotion &&\n          nowMs - lastPaint < REDUCED_MOTION_INTERVAL_MS\n        )\n          return;\n        lastPaint = nowMs;\n\n        const geometry: ElectricTraceGeometry = {\n          height: size.height,\n          width: size.width,\n          lineWidth: props.lineWidth ?? DEFAULT_LINE_WIDTH,\n        };\n\n        const nextActive = trace.step(nowMs, frame, geometry);\n\n        if (nextActive !== active) {\n          active = nextActive;\n          container.toggleAttribute(\"data-active\", active);\n        }\n\n        const drawing: PaintInput = {\n          colors,\n          geometry,\n          intensity: options.reducedMotion ? 0 : strength,\n          nowMs,\n          trace,\n        };\n\n        clearElectricCanvas(glowContext, size, size.glowRatio);\n        paintGlow(glowContext, drawing);\n        clearElectricCanvas(mainContext, size, size.ratio);\n        paintMain(mainContext, drawing);\n\n        if (props.fadeEdges ?? true) {\n          fadeEnds(glowContext, geometry);\n          fadeEnds(mainContext, geometry);\n        }\n      });\n\n      return () => {\n        unsubscribe();\n        observer.disconnect();\n        container.removeAttribute(\"data-active\");\n      };\n    }\n  );\n\n  const rest = omitProps(props, [\n    \"ref\",\n    \"class\",\n    \"className\",\n    \"source\",\n    \"mode\",\n    \"loading\",\n    \"intensity\",\n    \"arcs\",\n    \"sparks\",\n    \"sensitivity\",\n    \"lineWidth\",\n    \"fadeEdges\",\n    \"actionsRef\",\n  ]);\n\n  return (\n    <div\n      aria-label=\"Audio waveform\"\n      class={cn(\"relative h-24 w-full\", props.class, props.className)}\n      data-loading={props.loading ? \"\" : undefined}\n      data-mode={props.mode ?? \"wave\"}\n      data-slot=\"electric-waveform\"\n      role=\"img\"\n      {...rest}\n      ref={(node: HTMLDivElement) => {\n        root = node;\n        setRefValue(props.ref, node);\n      }}\n    >\n      <canvas\n        aria-hidden=\"true\"\n        class={ELECTRIC_GLOW_CLASS}\n        data-slot=\"electric-waveform-glow\"\n        ref={(node: HTMLCanvasElement) => {\n          glow = node;\n        }}\n      />\n      <canvas\n        aria-hidden=\"true\"\n        class={ELECTRIC_CANVAS_CLASS}\n        data-slot=\"electric-waveform-canvas\"\n        ref={(node: HTMLCanvasElement) => {\n          main = node;\n        }}\n      />\n    </div>\n  );\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
