diff --git a/src/components/Hero/AudioVisualizer3D.jsx b/src/components/Hero/AudioVisualizer3D.jsx
new file mode 100644
index 0000000..c21d73d
--- /dev/null
+++ b/src/components/Hero/AudioVisualizer3D.jsx
@@ -0,0 +1,92 @@
+import { useRef, useMemo } from 'react'
+import { Canvas, useFrame } from '@react-three/fiber'
+import * as THREE from 'three'
+
+const BAR_COUNT = 80
+
+function Bars({ isPlaying }) {
+ const barsRef = useRef([])
+ const phases = useMemo(() => Array.from({ length: BAR_COUNT }, () => Math.random() * Math.PI * 2), [])
+ const speeds = useMemo(() => Array.from({ length: BAR_COUNT }, () => 0.5 + Math.random() * 1.6), [])
+
+ useFrame(({ clock }, delta) => {
+ const t = clock.getElapsedTime()
+ barsRef.current.forEach((mesh, i) => {
+ if (!mesh) return
+ const target = isPlaying
+ ? Math.abs(Math.sin(t * speeds[i] + phases[i])) * 2.4 + 0.06
+ : Math.abs(Math.sin(t * 0.22 + phases[i])) * 0.28 + 0.04
+ mesh.scale.y = THREE.MathUtils.lerp(mesh.scale.y, target, 0.09)
+ mesh.position.y = mesh.scale.y * 0.5
+ // Subtle colour shift along ring
+ const hue = 0.12 + (i / BAR_COUNT) * 0.04
+ mesh.material.emissiveIntensity = 0.5 + mesh.scale.y * 0.25
+ })
+ })
+
+ const positions = useMemo(() => {
+ const r = 3.4
+ return Array.from({ length: BAR_COUNT }, (_, i) => {
+ const a = (i / BAR_COUNT) * Math.PI * 2
+ return { x: Math.cos(a) * r, z: Math.sin(a) * r }
+ })
+ }, [])
+
+ return (
+
+ {positions.map((p, i) => (
+ (barsRef.current[i] = el)} position={[p.x, 0.5, p.z]}>
+
+
+
+ ))}
+
+ {/* Outer ring */}
+
+
+
+
+
+ {/* Inner ring */}
+
+
+
+
+
+ {/* Centre glow disc */}
+
+
+
+
+
+ )
+}
+
+function Scene({ isPlaying }) {
+ const groupRef = useRef()
+ useFrame((_, delta) => {
+ if (groupRef.current) groupRef.current.rotation.y += delta * (isPlaying ? 0.38 : 0.07)
+ })
+ return (
+
+
+
+ )
+}
+
+export default function AudioVisualizer3D({ isPlaying }) {
+ return (
+
+ )
+}