{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "electric-bar-visualizer",
  "title": "Electric Bar Visualizer",
  "description": "Frequency-band bars drawn as crackling filaments.",
  "dependencies": [
    "solid-js@^1.9.15"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/electric",
    "@audiocn-solid/use-audio-config",
    "@audiocn-solid/use-frame-source",
    "@audiocn-solid/use-reduced-motion",
    "@audiocn-solid/use-visibility"
  ],
  "files": [
    {
      "path": "components/ui/electric-bar-visualizer.tsx",
      "content": "import { useAudioConfig } from \"@/hooks/use-audio-config\";\nimport { useFrameSource } from \"@/hooks/use-frame-source\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibility } from \"@/hooks/use-visibility\";\nimport { createBarLevels } from \"@/lib/audio/bar-levels\";\nimport type { BarIdle } from \"@/lib/audio/bar-levels\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport { subscribeFrame } from \"@/lib/audio/frame-loop\";\nimport type { FrameSource, Orientation, VisualFrame } from \"@/lib/audio/types\";\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_BAR_COUNT = 16;\n\nconst DEFAULT_MIN_LEVEL = 0.08;\n\nconst DEFAULT_INTENSITY = 0.6;\n\nconst DEFAULT_BAR_WIDTH = 6;\n\nconst DEFAULT_BAR_GAP = 4;\n\nconst REDUCED_MOTION_INTERVAL_MS = 250;\n\nconst COLOR_REFRESH_FRAMES = 30;\n\nconst MS_PER_SECOND = 1000;\n\nconst FRAME_SECONDS = 1 / 60;\n\nconst MAX_STEP_SECONDS = 0.05;\n\n/** A filament has 2^4 segments, so every split halves one cleanly. */\nconst FILAMENT_DEPTH = 4;\n\nconst FILAMENT_POINTS = 2 ** FILAMENT_DEPTH + 1;\n\n/** Shortest segment in pixels before a filament drops to fewer points. */\nconst SEGMENT_PX = 5;\n\n/** How much each finer split bends compared with the one before. */\nconst ROUGHNESS = 0.72;\n\n/** How far a free tip wanders, relative to the jitter amplitude. */\nconst FREE_TIP = 0.5;\n\n/** Each jagged shape holds at least this long, so the crackle never strobes. */\nconst JITTER_MS = 42;\n\n/** Largest bend, relative to the filament's length. */\nconst MAX_BEND = 0.35;\n\n/** Brightness flicker, at most 12%. */\nconst FLICKER = 0.12;\n\n/** Tips start to glow above this level. */\nconst TIP_LEVEL = 0.25;\n\nconst MAX_ARCS = 3;\n\n/** Arcs jump to the next bar or the one after. */\nconst MAX_ARC_REACH = 2;\n\nconst ARC_DEPTH = 3;\n\nconst ARC_POINTS = 2 ** ARC_DEPTH + 1;\n\nconst ARC_ATTEMPTS = 2;\n\n/** Both neighbours must be this loud for an arc to jump between them. */\nconst ARC_THRESHOLD = 0.45;\n\nconst ARC_CHANCE = 0.35;\n\nconst ARC_MIN_MS = 70;\n\nconst ARC_MAX_MS = 150;\n\n/** How far an arc bends, relative to the distance it spans. */\nconst ARC_BOW = 0.3;\n\nconst MAX_SPARKS = 64;\n\n/** A rise this big in one frame throws sparks. */\nconst SPARK_RISE = 0.12;\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 = 180;\n\n/** Pixels per second, sideways. */\nconst SPARK_SPREAD = 70;\n\nconst SPARK_MIN_MS = 260;\n\nconst SPARK_MAX_MS = 560;\n\nconst SPARK_TRAIL_SECONDS = 0.03;\n\ntype Align = \"center\" | \"start\" | \"end\";\n\ntype Tip = \"from\" | \"to\";\n\nexport interface ElectricBarVisualizerActions {\n  /** Paint levels (0..1) directly. They are resampled to the bar count. */\n  paint: (levels: ArrayLike<number>) => void;\n}\n\nexport interface ElectricBarVisualizerProps extends Omit<DivDOMProps, \"ref\"> {\n  /** A visual source; the bars follow its frequency bands. */\n  source?: FrameSource<VisualFrame> | null;\n  /** Levels, 0..1, for declarative use. */\n  levels?: ArrayLike<number>;\n  /** Number of bars. Bands are resampled to fit. Default 16. */\n  barCount?: number;\n  /** Where bars grow from. Default `center`. */\n  align?: Align;\n  /** Symmetric around the middle bar. Default false. */\n  mirrored?: boolean;\n  /** Resting bar size, 0..1. Default 0.08. */\n  minLevel?: number;\n  /** What the bars do with no signal. Default `static`. */\n  idle?: BarIdle;\n  /** Runs an arc along the bars, for connecting or thinking states. Default false. */\n  loading?: boolean;\n  orientation?: Orientation;\n  /** How jagged and restless the filaments are, 0..1. Default 0.6. */\n  intensity?: number;\n  /** Arcs jump between loud neighbouring bars. Default true. */\n  arcs?: boolean;\n  /** Sparks fly off the tips on sudden rises. Default true. */\n  sparks?: boolean;\n  /** Widest bar in pixels. Default 6. */\n  barWidth?: number;\n  /** Gap between bars in pixels. Default 4. */\n  barGap?: number;\n  actionsRef?: RefTarget<ElectricBarVisualizerActions>;\n  ref?: RefTarget<HTMLDivElement>;\n  className?: string;\n}\n\n/**\n * Where the bars sit, in CSS pixels. `across` runs along the row of bars and\n * `along` runs the way they grow, so both orientations share one drawing.\n */\nexport interface ElectricLayout {\n  horizontal: boolean;\n  align: Align;\n  barWidth: number;\n  barGap: number;\n  /** Centre of the first bar, across. */\n  first: number;\n  /** Distance between bar centres. */\n  pitch: number;\n  /** Where bars grow from, along: the base, or the middle when centred. */\n  base: number;\n  /** Length of a bar at level 1. */\n  span: number;\n}\n\nexport interface ElectricLayoutOptions {\n  align: Align;\n  barCount: number;\n  barGap: number;\n  barWidth: number;\n  orientation: Orientation;\n}\n\n/** Fits the bars into a canvas, narrowing bars and gaps when space is short. */\nexport const layoutElectricBars = (\n  width: number,\n  height: number,\n  { align, barCount, barGap, barWidth, orientation }: ElectricLayoutOptions\n): ElectricLayout => {\n  const horizontal = orientation === \"horizontal\";\n  const count = Math.max(1, barCount);\n  const acrossLength = horizontal ? width : height;\n  const alongLength = horizontal ? height : width;\n\n  const pitch = Math.min(\n    barWidth + barGap,\n    Math.max(0, acrossLength - barWidth * 2) / count\n  );\n\n  const scale = pitch / (barWidth + barGap || 1);\n  const resolvedWidth = barWidth * scale;\n  const resolvedGap = barGap * scale;\n  const inset = Math.min(resolvedWidth, alongLength / 4);\n\n  const baseFor = {\n    center: alongLength / 2,\n    end: alongLength - inset,\n    start: inset,\n  };\n\n  return {\n    align,\n    barGap: resolvedGap,\n    barWidth: resolvedWidth,\n    base: baseFor[align],\n    first: (acrossLength - count * pitch + resolvedGap + resolvedWidth) / 2,\n    horizontal,\n    pitch,\n    span: Math.max(0, alongLength - inset * 2),\n  };\n};\n\nconst barFrom = (layout: ElectricLayout, level: number) =>\n  layout.align === \"center\"\n    ? layout.base - (level * layout.span) / 2\n    : layout.base;\n\nconst barTo = (layout: ElectricLayout, level: number) => {\n  if (layout.align === \"center\") {\n    return layout.base + (level * layout.span) / 2;\n  }\n\n  const direction = layout.align === \"end\" ? -1 : 1;\n\n  return layout.base + direction * level * layout.span;\n};\n\n/** Which way a tip points, along: sparks fly that way. */\nconst tipDirection = (layout: ElectricLayout, tip: Tip) => {\n  const growth = layout.align === \"end\" ? -1 : 1;\n\n  return tip === \"to\" ? growth : -growth;\n};\n\n/**\n * How far a filament wanders sideways, in pixels. Louder bars crackle more,\n * and short ones never bend more than a third of their length.\n */\nconst jitterAmplitude = (\n  layout: ElectricLayout,\n  intensity: number,\n  level: number\n) =>\n  Math.min(\n    intensity * (layout.barWidth + layout.barGap) * 0.6 * (0.3 + 0.7 * level),\n    level * layout.span * MAX_BEND\n  );\n\nexport interface ElectricArc {\n  /** The arc joins this bar and the one `reach` bars on. */\n  index: number;\n  reach: number;\n  /** Which end of the bars it joins: `to` is the tip, `from` the base (centred bars only). */\n  tip: Tip;\n  bornMs: number;\n  /** 0 when the arc is gone. */\n  lifeMs: number;\n  /** Bend at each point, in units of the arc's bow. */\n  jitter: Float32Array;\n}\n\nexport interface ElectricSceneOptions {\n  barCount: number;\n  intensity: number;\n  arcs: boolean;\n  sparks: boolean;\n  loading: boolean;\n  reducedMotion: boolean;\n  /** Makes the crackle repeatable. Each scene gets its own by default. */\n  seed?: number;\n}\n\nexport interface ElectricScene {\n  /** Sideways offset of each filament point, per bar, in units of its jitter amplitude. */\n  jitter: Float32Array;\n  /** Brightness of each bar, 0.88..1. */\n  flicker: Float32Array;\n  arcs: ElectricArc[];\n  sparks: ElectricSparks;\n  /** Advances the crackle, arcs and sparks to `nowMs`. */\n  step: (nowMs: number, levels: Float32Array, layout: ElectricLayout) => void;\n}\n\nconst loudestIndex = (levels: Float32Array) => {\n  let loudest = 0;\n\n  for (const [index, level] of levels.entries()) {\n    if (level > (levels[loudest] ?? 0)) {\n      loudest = index;\n    }\n  }\n\n  return loudest;\n};\n\nlet sceneCount = 0;\n\n/**\n * The moving parts of the electric look, apart from any canvas: jagged\n * filaments, arcs between loud neighbours and sparks thrown off sudden rises.\n * With reduced motion it stays straight and still.\n */\nexport const createElectricScene = ({\n  arcs,\n  barCount,\n  intensity,\n  loading,\n  reducedMotion,\n  seed,\n  sparks,\n}: ElectricSceneOptions): ElectricScene => {\n  sceneCount += 1;\n  const random = createRandom(seed ?? sceneCount);\n  const signed = () => random() * 2 - 1;\n  const jitter = new Float32Array(barCount * FILAMENT_POINTS);\n  const flicker = new Float32Array(barCount).fill(1);\n\n  const arcList: ElectricArc[] = Array.from({ length: MAX_ARCS }, () => ({\n    bornMs: 0,\n    index: 0,\n    jitter: new Float32Array(ARC_POINTS),\n    lifeMs: 0,\n    reach: 1,\n    tip: \"to\",\n  }));\n\n  const pool = createElectricSparks(MAX_SPARKS);\n  const previous = new Float32Array(barCount);\n  const lastEmitMs = new Float64Array(barCount).fill(-Infinity);\n  let lastMs = 0;\n  let lastJitterMs = -Infinity;\n  let primed = false;\n\n  const rerollFilaments = (centered: boolean) => {\n    for (let index = 0; index < barCount; index += 1) {\n      const from = centered ? signed() * FREE_TIP : 0;\n      displace(\n        jitter,\n        index * FILAMENT_POINTS,\n        FILAMENT_POINTS,\n        [from, signed() * FREE_TIP],\n        random,\n        ROUGHNESS\n      );\n      flicker[index] = 1 - FLICKER * random();\n    }\n  };\n\n  const startArc = (\n    arc: ElectricArc,\n    [index, reach]: [number, number],\n    tip: Tip,\n    nowMs: number\n  ) => {\n    arc.index = index;\n    arc.reach = reach;\n    arc.tip = tip;\n    arc.bornMs = nowMs;\n    arc.lifeMs = ARC_MIN_MS + random() * (ARC_MAX_MS - ARC_MIN_MS);\n  };\n\n  /** While loading, one arc rides the sweep across the bars. */\n  const followSweep = (\n    nowMs: number,\n    levels: Float32Array,\n    centered: boolean\n  ) => {\n    const peak = loudestIndex(levels);\n    const leansLeft = (levels[peak - 1] ?? 0) > (levels[peak + 1] ?? 0);\n    const index = clamp(leansLeft ? peak - 1 : peak, 0, barCount - 2);\n    const [arc] = arcList;\n\n    if (arc) {\n      startArc(arc, [index, 1], centered ? \"from\" : \"to\", nowMs);\n      arc.lifeMs = JITTER_MS * 2;\n    }\n  };\n\n  const spawnArcs = (\n    nowMs: number,\n    levels: Float32Array,\n    centered: boolean\n  ) => {\n    for (let attempt = 0; attempt < ARC_ATTEMPTS; attempt += 1) {\n      const reach = 1 + Math.floor(random() * MAX_ARC_REACH);\n      const index = Math.floor(random() * Math.max(0, barCount - reach));\n      const strength = Math.min(levels[index] ?? 0, levels[index + reach] ?? 0);\n\n      const charged =\n        strength >= ARC_THRESHOLD &&\n        random() < ARC_CHANCE * intensity * strength;\n\n      const free = arcList.find((arc) => arc.lifeMs === 0);\n\n      const taken = arcList.some(\n        (arc) => arc.lifeMs > 0 && arc.index === index\n      );\n\n      if (charged && free && !taken) {\n        startArc(\n          free,\n          [index, reach],\n          centered && random() < 0.5 ? \"from\" : \"to\",\n          nowMs\n        );\n      }\n    }\n  };\n\n  const updateArcs = (\n    nowMs: number,\n    levels: Float32Array,\n    centered: boolean\n  ) => {\n    if (barCount < 2) {\n      return;\n    }\n\n    if (loading) {\n      followSweep(nowMs, levels, centered);\n    } else {\n      spawnArcs(nowMs, levels, centered);\n    }\n\n    for (const arc of arcList) {\n      if (arc.lifeMs > 0) {\n        displace(arc.jitter, 0, ARC_POINTS, [0, 0], random, ROUGHNESS);\n      }\n    }\n  };\n\n  const expireArcs = (nowMs: number) => {\n    for (const arc of arcList) {\n      if (arc.lifeMs > 0 && nowMs - arc.bornMs >= arc.lifeMs) {\n        arc.lifeMs = 0;\n      }\n    }\n  };\n\n  const launchSpark = (\n    index: number,\n    level: number,\n    layout: ElectricLayout\n  ) => {\n    const tip: Tip =\n      layout.align === \"center\" && random() < 0.5 ? \"from\" : \"to\";\n\n    const along = tip === \"to\" ? barTo(layout, level) : barFrom(layout, level);\n    const across = layout.first + index * layout.pitch;\n    const speed = SPARK_SPEED * (0.5 + random()) * (0.5 + intensity);\n    const alongVelocity = tipDirection(layout, tip) * speed;\n    const acrossVelocity = signed() * SPARK_SPREAD;\n    emitSpark(pool, {\n      lifeMs: SPARK_MIN_MS + random() * (SPARK_MAX_MS - SPARK_MIN_MS),\n      vx: layout.horizontal ? acrossVelocity : alongVelocity,\n      vy: layout.horizontal ? alongVelocity : acrossVelocity,\n      x: layout.horizontal ? across : along,\n      y: layout.horizontal ? along : across,\n    });\n  };\n\n  const emitSparks = (\n    nowMs: number,\n    levels: Float32Array,\n    layout: ElectricLayout\n  ) => {\n    for (const [index, level] of levels.entries()) {\n      const rise = level - (previous[index] ?? 0);\n      const rested = nowMs - (lastEmitMs[index] ?? 0) >= SPARK_COOLDOWN_MS;\n\n      if (rise >= SPARK_RISE && level >= SPARK_MIN_LEVEL && rested) {\n        lastEmitMs[index] = nowMs;\n        const count = 2 + Math.floor(random() * 3);\n\n        for (let spark = 0; spark < count; spark += 1) {\n          launchSpark(index, level, layout);\n        }\n      }\n    }\n  };\n\n  const step = (\n    nowMs: number,\n    levels: Float32Array,\n    layout: ElectricLayout\n  ) => {\n    const seconds =\n      lastMs === 0\n        ? FRAME_SECONDS\n        : clamp((nowMs - lastMs) / MS_PER_SECOND, 0, MAX_STEP_SECONDS);\n\n    lastMs = nowMs;\n\n    if (reducedMotion) {\n      return;\n    }\n\n    const centered = layout.align === \"center\";\n\n    if (nowMs - lastJitterMs >= JITTER_MS) {\n      lastJitterMs = nowMs;\n      rerollFilaments(centered);\n\n      if (arcs) {\n        updateArcs(nowMs, levels, centered);\n      }\n    }\n\n    expireArcs(nowMs);\n\n    if (sparks) {\n      if (primed) {\n        emitSparks(nowMs, levels, layout);\n      }\n\n      moveSparks(pool, seconds, SPARK_GRAVITY);\n    }\n\n    previous.set(levels);\n    primed = true;\n  };\n\n  return { arcs: arcList, flicker, jitter, sparks: pool, step };\n};\n\ninterface PaintInput {\n  scene: ElectricScene;\n  layout: ElectricLayout;\n  levels: Float32Array;\n  colors: ElectricColors;\n  intensity: number;\n  nowMs: number;\n}\n\nconst plot = (\n  context: CanvasRenderingContext2D,\n  layout: ElectricLayout,\n  across: number,\n  along: number,\n  move: boolean\n) => {\n  const x = layout.horizontal ? across : along;\n  const y = layout.horizontal ? along : across;\n\n  if (move) {\n    context.moveTo(x, y);\n  } else {\n    context.lineTo(x, y);\n  }\n};\n\n/** Short filaments use fewer points, so they stay a line and not a scribble. */\nconst strideFor = (length: number) => {\n  const depth = clamp(\n    Math.ceil(Math.log2(Math.max(1, length) / SEGMENT_PX)),\n    1,\n    FILAMENT_DEPTH\n  );\n\n  return 2 ** (FILAMENT_DEPTH - depth);\n};\n\n/** How one pass draws the filaments. */\ninterface FilamentPass {\n  /** Line width, relative to the bar width. */\n  width: number;\n  /** Least line width in pixels. */\n  minWidth: number;\n  /** Share of the jitter the pass follows: the sheath stays close to a bar while the core crackles. */\n  bend: number;\n  join: CanvasLineJoin;\n  alpha: (level: number) => number;\n}\n\nconst GLOW_PASS: FilamentPass = {\n  alpha: (level) => 0.1 + 0.35 * level,\n  bend: 0.35,\n  join: \"round\",\n  minWidth: 2,\n  width: 1.6,\n};\n\nconst SHEATH_PASS: FilamentPass = {\n  alpha: (level) => 0.35 + 0.35 * level,\n  bend: 0.35,\n  join: \"round\",\n  minWidth: 1,\n  width: 1,\n};\n\nconst CORE_PASS: FilamentPass = {\n  alpha: (level) => 0.8 + 0.2 * level,\n  bend: 1,\n  join: \"miter\",\n  minWidth: 1.25,\n  width: 0.3,\n};\n\nconst traceFilament = (\n  context: CanvasRenderingContext2D,\n  { intensity, layout, scene }: PaintInput,\n  index: number,\n  level: number,\n  bendShare: number\n) => {\n  const across = layout.first + index * layout.pitch;\n  const amplitude = jitterAmplitude(layout, intensity, level) * bendShare;\n  const from = barFrom(layout, level);\n  const to = barTo(layout, level);\n  const stride = strideFor(Math.abs(to - from));\n  const offset = index * FILAMENT_POINTS;\n  context.beginPath();\n\n  for (let point = 0; point < FILAMENT_POINTS; point += stride) {\n    const along = from + ((to - from) * point) / (FILAMENT_POINTS - 1);\n    const bend = (scene.jitter[offset + point] ?? 0) * amplitude;\n    plot(context, layout, across + bend, along, point === 0);\n  }\n};\n\nconst strokeFilaments = (\n  context: CanvasRenderingContext2D,\n  input: PaintInput,\n  pass: FilamentPass\n) => {\n  context.lineWidth = Math.max(\n    pass.minWidth,\n    input.layout.barWidth * pass.width\n  );\n  context.lineJoin = pass.join;\n\n  for (const [index, level] of input.levels.entries()) {\n    const flicker = input.scene.flicker[index] ?? 1;\n    context.globalAlpha = clamp(pass.alpha(level) * flicker, 0, 1);\n    traceFilament(context, input, index, level, pass.bend);\n    context.stroke();\n  }\n};\n\n/** Where a bar's tip is, jitter included, as `[across, along]`. */\nconst tipOf = (\n  { intensity, layout, scene }: PaintInput,\n  index: number,\n  level: number,\n  tip: Tip\n): [number, number] => {\n  const point = tip === \"to\" ? FILAMENT_POINTS - 1 : 0;\n\n  const bend =\n    (scene.jitter[index * FILAMENT_POINTS + point] ?? 0) *\n    jitterAmplitude(layout, intensity, level);\n\n  const along = tip === \"to\" ? barTo(layout, level) : barFrom(layout, level);\n\n  return [layout.first + index * layout.pitch + bend, along];\n};\n\nconst fillTips = (\n  context: CanvasRenderingContext2D,\n  input: PaintInput,\n  radius: number\n) => {\n  const tips: Tip[] = input.layout.align === \"center\" ? [\"from\", \"to\"] : [\"to\"];\n\n  for (const [index, level] of input.levels.entries()) {\n    const heat = (level - TIP_LEVEL) / (1 - TIP_LEVEL);\n\n    if (heat > 0) {\n      context.globalAlpha =\n        clamp(heat, 0, 1) * (input.scene.flicker[index] ?? 1);\n      context.beginPath();\n\n      for (const tip of tips) {\n        const [across, along] = tipOf(input, index, level, tip);\n        const x = input.layout.horizontal ? across : along;\n        const y = input.layout.horizontal ? along : across;\n        context.moveTo(x + radius, y);\n        context.arc(x, y, radius, 0, Math.PI * 2);\n      }\n\n      context.fill();\n    }\n  }\n};\n\nconst traceArc = (\n  context: CanvasRenderingContext2D,\n  input: PaintInput,\n  arc: ElectricArc\n) => {\n  const { layout, levels } = input;\n\n  const [fromAcross, fromAlong] = tipOf(\n    input,\n    arc.index,\n    levels[arc.index] ?? 0,\n    arc.tip\n  );\n\n  const [toAcross, toAlong] = tipOf(\n    input,\n    arc.index + arc.reach,\n    levels[arc.index + arc.reach] ?? 0,\n    arc.tip\n  );\n\n  const acrossSpan = toAcross - fromAcross;\n  const alongSpan = toAlong - fromAlong;\n  const length = Math.hypot(acrossSpan, alongSpan) || 1;\n  const bow = length * ARC_BOW;\n  context.beginPath();\n\n  for (let point = 0; point < ARC_POINTS; point += 1) {\n    const progress = point / (ARC_POINTS - 1);\n    const bend = (arc.jitter[point] ?? 0) * bow;\n    plot(\n      context,\n      layout,\n      fromAcross + acrossSpan * progress - (alongSpan / length) * bend,\n      fromAlong + alongSpan * progress + (acrossSpan / length) * bend,\n      point === 0\n    );\n  }\n};\n\nconst strokeArcs = (\n  context: CanvasRenderingContext2D,\n  input: PaintInput,\n  lineWidth: number,\n  alpha: number\n) => {\n  context.lineWidth = lineWidth;\n\n  for (const arc of input.scene.arcs) {\n    if (arc.lifeMs > 0) {\n      const age = (input.nowMs - arc.bornMs) / arc.lifeMs;\n      context.globalAlpha = clamp(alpha * (1 - age * age), 0, 1);\n      traceArc(context, input, arc);\n      context.stroke();\n    }\n  }\n};\n\n/** Wide, soft strokes. The canvas is blurred with CSS, which is far cheaper than `shadowBlur`. */\nconst paintGlow = (context: CanvasRenderingContext2D, input: PaintInput) => {\n  const { barWidth } = input.layout;\n  context.strokeStyle = input.colors.glow;\n  context.fillStyle = input.colors.glow;\n  strokeFilaments(context, input, GLOW_PASS);\n  fillTips(context, input, barWidth * 1.2);\n  context.lineJoin = \"round\";\n  strokeArcs(context, input, barWidth, 0.7);\n};\n\nconst paintMain = (context: CanvasRenderingContext2D, input: PaintInput) => {\n  const { barWidth } = input.layout;\n  context.strokeStyle = input.colors.body;\n  strokeFilaments(context, input, SHEATH_PASS);\n  context.lineJoin = \"miter\";\n  strokeArcs(context, input, Math.max(1.5, barWidth * 0.45), 0.6);\n  context.strokeStyle = input.colors.core;\n  context.fillStyle = input.colors.core;\n  strokeFilaments(context, input, CORE_PASS);\n  strokeArcs(context, input, 1, 1);\n  fillTips(context, input, Math.max(1, barWidth * 0.35));\n  strokeSparks(\n    context,\n    input.scene.sparks,\n    Math.max(1, barWidth * 0.25),\n    SPARK_TRAIL_SECONDS\n  );\n};\n\nexport const ElectricBarVisualizer = (props: ElectricBarVisualizerProps) => {\n  const config = useAudioConfig();\n  const reducedMotion = useReducedMotion();\n\n  const orientation = () =>\n    props.orientation ?? config.orientation ?? \"horizontal\";\n\n  let root: HTMLDivElement | undefined;\n  let main: HTMLCanvasElement | undefined;\n  let glow: HTMLCanvasElement | undefined;\n  let input: ArrayLike<number> | null = null;\n  let hadLevels = false;\n  const visible = useVisibility(() => root ?? null);\n\n  const paint = (next: ArrayLike<number>) => {\n    input = next;\n  };\n\n  useFrameSource(\n    () => props.source,\n    (frame) => paint(frame.bands)\n  );\n  createCompatEffect(\n    () => props.levels,\n    (levels) => {\n      if (levels) {\n        input = levels;\n        hadLevels = true;\n      } else if (hadLevels) {\n        input = null;\n        hadLevels = false;\n      }\n    }\n  );\n  createCompatEffect(\n    () => props.actionsRef,\n    (ref) => {\n      setRefValue(ref, { paint });\n\n      return () => setRefValue(ref, null);\n    }\n  );\n  createCompatEffect(\n    () => ({\n      align: props.align ?? \"center\",\n      arcs: props.arcs ?? true,\n      barCount: props.barCount ?? DEFAULT_BAR_COUNT,\n      barGap: props.barGap ?? DEFAULT_BAR_GAP,\n      barWidth: props.barWidth ?? DEFAULT_BAR_WIDTH,\n      idle: props.idle ?? \"static\",\n      intensity: props.intensity ?? DEFAULT_INTENSITY,\n      loading: props.loading ?? false,\n      minLevel: props.minLevel ?? DEFAULT_MIN_LEVEL,\n      mirrored: props.mirrored ?? false,\n      orientation: orientation(),\n      reducedMotion: reducedMotion(),\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 barLevels = createBarLevels(options);\n      const scene = createElectricScene({ ...options, intensity: strength });\n\n      let size: ElectricCanvasSize = {\n        glowRatio: 1,\n        height: 0,\n        ratio: 1,\n        width: 0,\n      };\n\n      let layout = layoutElectricBars(0, 0, options);\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        layout = layoutElectricBars(size.width, size.height, options);\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        const nextActive = barLevels.step(nowMs, input);\n\n        if (nextActive !== active) {\n          active = nextActive;\n          container.toggleAttribute(\"data-active\", active);\n        }\n\n        scene.step(nowMs, barLevels.levels, layout);\n\n        const drawing: PaintInput = {\n          colors,\n          intensity: options.reducedMotion ? 0 : strength,\n          layout,\n          levels: barLevels.levels,\n          nowMs,\n          scene,\n        };\n\n        clearElectricCanvas(glowContext, size, size.glowRatio);\n        paintGlow(glowContext, drawing);\n        clearElectricCanvas(mainContext, size, size.ratio);\n        paintMain(mainContext, drawing);\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    \"levels\",\n    \"barCount\",\n    \"align\",\n    \"mirrored\",\n    \"minLevel\",\n    \"idle\",\n    \"loading\",\n    \"orientation\",\n    \"intensity\",\n    \"arcs\",\n    \"sparks\",\n    \"barWidth\",\n    \"barGap\",\n    \"actionsRef\",\n  ]);\n\n  return (\n    <div\n      aria-label=\"Audio visualizer\"\n      class={cn(\n        \"relative\",\n        orientation() === \"horizontal\" ? \"h-16 w-full\" : \"h-full w-16\",\n        props.class,\n        props.className\n      )}\n      data-align={props.align ?? \"center\"}\n      data-loading={props.loading ? \"\" : undefined}\n      data-orientation={orientation()}\n      data-slot=\"electric-bar-visualizer\"\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-bar-visualizer-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-bar-visualizer-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"
}
