{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "level-bars",
  "title": "Level Bars",
  "description": "Bars that rise and fall with a voice while someone speaks and settle when it goes quiet.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/speech-envelope.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/level-bars.tsx",
      "content": "'use client';\n\nimport { useReducedMotion } from 'motion/react';\nimport { useEffect, useRef } from 'react';\n\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport { speechLevelAt } from '@/registry/ndeko/lib/speech-envelope';\n\nconst SAMPLE_EVERY_MS = 70;\n\nconst FLOOR = 0.16;\n\n/** How much of the gap to the target intensity is closed on each frame, so silence settles instead of snapping. */\nconst INTENSITY_EASING = 0.12;\n\nfunction restingScaleOf(index: number, seed: number) {\n  const sampleTime = index * 140;\n  const level = speechLevelAt(sampleTime, seed + 1.3);\n  return Math.max(FLOOR, level);\n}\n\nfunction restingStyleOf(index: number, seed: number) {\n  const scale = restingScaleOf(index, seed);\n  return { transform: `scaleY(${scale})` };\n}\n\nfunction barsOf(container: HTMLElement) {\n  const children = Array.from(container.children);\n  return children.filter((child) => child instanceof HTMLElement);\n}\n\nfunction paint(bars: HTMLElement[], samples: number[]) {\n  bars.forEach((bar, index) => {\n    const sample = samples[index] ?? FLOOR;\n    bar.style.transform = `scaleY(${sample})`;\n  });\n}\n\ntype LevelBarsProps = {\n  isActive: boolean;\n  /** Scales the simulated voice: 1 while someone speaks, near 0 in silence. */\n  intensity?: number;\n  /** A live level between 0 and 1, such as a microphone analyser. Left out, a simulated voice drives the bars. */\n  level?: () => number;\n  count?: number;\n  seed?: number;\n  className?: string;\n  barClassName?: string;\n};\n\n/** Frames are painted through the DOM, so React never re-renders per frame. */\nexport function LevelBars({\n  isActive,\n  intensity = 1,\n  level,\n  count = 16,\n  seed = 0,\n  className,\n  barClassName,\n}: LevelBarsProps) {\n  const containerRef = useRef<HTMLSpanElement>(null);\n  const intensityRef = useRef(intensity);\n  const levelRef = useRef(level);\n  const shouldReduceMotion = useReducedMotion();\n  const indexes = Array.from({ length: count }, (_, index) => index);\n\n  useEffect(() => {\n    intensityRef.current = intensity;\n  }, [intensity]);\n\n  useEffect(() => {\n    levelRef.current = level;\n  }, [level]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n    const shouldAnimate = isActive && !shouldReduceMotion;\n    if (!shouldAnimate) {\n      return;\n    }\n    const bars = barsOf(container);\n    const samples = bars.map((_, index) => restingScaleOf(index, seed));\n    const startedAt = performance.now();\n    let currentIntensity = intensityRef.current;\n    let lastSampleAt = -Infinity;\n    let frame = 0;\n\n    function sampleAt(now: number) {\n      const provider = levelRef.current;\n      if (provider) {\n        return provider();\n      }\n      const elapsed = now - startedAt;\n      const simulated = speechLevelAt(elapsed, seed);\n      return simulated * currentIntensity;\n    }\n\n    function draw(now: number) {\n      const gap = intensityRef.current - currentIntensity;\n      currentIntensity += gap * INTENSITY_EASING;\n      const sinceSample = now - lastSampleAt;\n      const isDue = sinceSample >= SAMPLE_EVERY_MS;\n      if (isDue) {\n        lastSampleAt = now;\n        const sample = sampleAt(now);\n        samples.shift();\n        samples.push(Math.max(FLOOR, sample));\n        paint(bars, samples);\n      }\n      frame = requestAnimationFrame(draw);\n    }\n\n    frame = requestAnimationFrame(draw);\n    return () => cancelAnimationFrame(frame);\n  }, [isActive, shouldReduceMotion, seed]);\n\n  return (\n    <span\n      ref={containerRef}\n      aria-hidden='true'\n      className={cn('flex h-5 items-center gap-[3px]', className)}\n    >\n      {indexes.map((index) => (\n        <span\n          key={index}\n          style={restingStyleOf(index, seed)}\n          className={cn(\n            'bg-primary h-full w-[3px] shrink-0 rounded-full transition-transform duration-75 ease-linear',\n            barClassName\n          )}\n        />\n      ))}\n    </span>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "ai"
  ],
  "type": "registry:ui"
}