{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fly-through",
  "type": "registry:component",
  "title": "FlyThrough",
  "description": "A free-flight 3D world: pilot a spaceship through an infinite wrap-around field of glowing prismatic crystals — cursor steers, hold to boost. Fog + bloom; the browser as a game engine.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/postprocessing"
  ],
  "registryDependencies": [
    "https://prism.icglabs.co/r/canvas-perf.json"
  ],
  "tier": "pro",
  "note": "A full mini-experience: mouse/WASD/scroll controls, a target-to-chase score HUD, and a toggleable generative-audio reactor. Render full-screen via next/dynamic ssr:false. The HUD uses Prism's color tokens (text-mist/border-line/bg-obsidian — copy from globals.css or restyle). Honors prefers-reduced-motion.",
  "files": [
    {
      "path": "components/prism/FlyThrough.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimport { EffectComposer, Bloom } from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport { useDeviceTier } from \"@/lib/useDeviceTier\";\nimport { AutoDpr, PauseWhenOffscreen } from \"@/components/showcase/CanvasPerf\";\n\n/**\n * FlyThrough — a free-flight 3D world you pilot like a spaceship through an\n * infinite, wrap-around field of glowing prismatic crystals. Mouse steers\n * (yaw/pitch), W/S or scroll throttles, A/D rolls, hold/Shift boosts. Chase the\n * bright marker for points. Toggle the generative soundtrack and the field pulses\n * to it. Fog hides the wrap; bloom makes it glow.\n *\n * Self-contained; renders its own HUD. Honors prefers-reduced-motion (static field).\n */\nconst PALETTE = [\"#8b5cff\", \"#5b7cff\", \"#1fd4e6\", \"#ff3d81\", \"#ffb24d\", \"#9bf25a\"];\n\ntype Controls = {\n  keys: Set<string>;\n  throttle: number; // wheel-adjusted base speed\n  boost: boolean;\n};\n\nfunction CrystalField({\n  count,\n  size,\n  reduce,\n  energy,\n}: {\n  count: number;\n  size: number;\n  reduce: boolean;\n  energy: React.RefObject<number>;\n}) {\n  const ref = useRef<THREE.InstancedMesh>(null);\n  const light = useRef<THREE.PointLight>(null);\n  const dummy = useMemo(() => new THREE.Object3D(), []);\n  const camera = useThree((s) => s.camera);\n\n  const data = useMemo(() => {\n    const colors = PALETTE.map((c) => new THREE.Color(c));\n    return Array.from({ length: count }, () => ({\n      base: new THREE.Vector3(\n        (Math.random() - 0.5) * size,\n        (Math.random() - 0.5) * size,\n        (Math.random() - 0.5) * size\n      ),\n      scale: 0.35 + Math.random() * 1.9,\n      color: colors[(Math.random() * colors.length) | 0],\n      rot: new THREE.Euler(Math.random() * 6.28, Math.random() * 6.28, Math.random() * 6.28),\n      spin: (Math.random() - 0.5) * 0.7,\n    }));\n  }, [count, size]);\n\n  useEffect(() => {\n    const m = ref.current;\n    if (!m) return;\n    data.forEach((d, i) => m.setColorAt(i, d.color));\n    if (m.instanceColor) m.instanceColor.needsUpdate = true;\n  }, [data]);\n\n  useFrame((_, dt) => {\n    const m = ref.current;\n    if (!m) return;\n    const cam = camera.position;\n    const step = Math.min(dt, 0.05);\n    const pulse = 1 + (energy.current ?? 0) * 0.35;\n    for (let i = 0; i < data.length; i++) {\n      const d = data[i];\n      if (!reduce) d.rot.y += d.spin * step;\n      const x = d.base.x - cam.x;\n      const y = d.base.y - cam.y;\n      const z = d.base.z - cam.z;\n      dummy.position.set(\n        cam.x + (x - size * Math.round(x / size)),\n        cam.y + (y - size * Math.round(y / size)),\n        cam.z + (z - size * Math.round(z / size))\n      );\n      dummy.rotation.copy(d.rot);\n      dummy.scale.setScalar(d.scale * pulse);\n      dummy.updateMatrix();\n      m.setMatrixAt(i, dummy.matrix);\n    }\n    m.instanceMatrix.needsUpdate = true;\n    if (light.current) {\n      light.current.position.copy(cam);\n      light.current.intensity = 22 + (energy.current ?? 0) * 55;\n    }\n  });\n\n  return (\n    <>\n      <ambientLight intensity={0.5} />\n      <directionalLight position={[3, 5, 2]} intensity={1.0} />\n      <pointLight ref={light} intensity={24} distance={62} decay={1.6} color=\"#9fd0ff\" />\n      <instancedMesh ref={ref} args={[undefined, undefined, count]}>\n        <octahedronGeometry args={[1, 0]} />\n        <meshStandardMaterial roughness={0.32} metalness={0.18} />\n      </instancedMesh>\n    </>\n  );\n}\n\nfunction Target({ onReach }: { onReach: () => void }) {\n  const ref = useRef<THREE.Mesh>(null);\n  const camera = useThree((s) => s.camera);\n  const pos = useRef(new THREE.Vector3(0, 0, -45));\n\n  const relocate = useCallback(() => {\n    const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);\n    const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);\n    const up = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);\n    pos.current\n      .copy(camera.position)\n      .addScaledVector(fwd, 55 + Math.random() * 45)\n      .addScaledVector(right, (Math.random() - 0.5) * 44)\n      .addScaledVector(up, (Math.random() - 0.5) * 30);\n  }, [camera]);\n\n  useEffect(() => {\n    relocate();\n  }, [relocate]);\n\n  useFrame((_, dt) => {\n    const m = ref.current;\n    if (!m) return;\n    m.position.copy(pos.current);\n    m.rotation.y += dt * 1.2;\n    m.rotation.x += dt * 0.7;\n    const s = 1 + Math.sin(performance.now() * 0.004) * 0.12;\n    m.scale.setScalar(s);\n    if (camera.position.distanceTo(pos.current) < 6) {\n      onReach();\n      relocate();\n    }\n  });\n\n  return (\n    <mesh ref={ref}>\n      <icosahedronGeometry args={[2, 0]} />\n      <meshStandardMaterial color=\"#ffffff\" emissive=\"#1fd4e6\" emissiveIntensity={2.4} toneMapped={false} />\n    </mesh>\n  );\n}\n\nfunction Pilot({ reduce, controls }: { reduce: boolean; controls: React.RefObject<Controls> }) {\n  const camera = useThree((s) => s.camera) as THREE.PerspectiveCamera;\n  const yawV = useRef(0);\n  const pitchV = useRef(0);\n  const rollV = useRef(0);\n  const spd = useRef(16);\n\n  useFrame((state, dt) => {\n    if (reduce) return;\n    const c = controls.current;\n    if (!c) return;\n    const step = Math.min(dt, 0.05);\n    const px = Math.abs(state.pointer.x) < 0.06 ? 0 : state.pointer.x;\n    const py = Math.abs(state.pointer.y) < 0.06 ? 0 : state.pointer.y;\n    yawV.current += (-px * 0.7 - yawV.current) * 0.06;\n    pitchV.current += (py * 0.5 - pitchV.current) * 0.06;\n    const rollInput = (c.keys.has(\"a\") ? 1 : 0) - (c.keys.has(\"d\") ? 1 : 0);\n    rollV.current += (rollInput * 1.1 - rollV.current) * 0.08;\n    camera.rotateY(yawV.current * step);\n    camera.rotateX(pitchV.current * step);\n    camera.rotateZ(rollV.current * step);\n\n    // throttle: scroll-wheel base + W/S nudge + boost\n    let target = c.throttle;\n    if (c.keys.has(\"w\")) target += 16;\n    if (c.keys.has(\"s\")) target -= 12;\n    if (c.boost || c.keys.has(\"shift\")) target += 26;\n    target = THREE.MathUtils.clamp(target, 4, 80);\n    spd.current += (target - spd.current) * 0.03;\n    camera.translateZ(-spd.current * step);\n\n    const fov = 56 + (spd.current - 16) * 0.45;\n    camera.fov = THREE.MathUtils.lerp(camera.fov, THREE.MathUtils.clamp(fov, 50, 92), 0.1);\n    camera.updateProjectionMatrix();\n  });\n  return null;\n}\n\nfunction AudioReactor({ enabled, energy }: { enabled: boolean; energy: React.RefObject<number> }) {\n  useEffect(() => {\n    if (!enabled) return;\n    const Ctx =\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;\n    if (!Ctx) return;\n    const ctx = new Ctx();\n    const master = ctx.createGain();\n    master.gain.value = 0.0001;\n    master.connect(ctx.destination);\n    const analyser = ctx.createAnalyser();\n    analyser.fftSize = 256;\n\n    const lp = ctx.createBiquadFilter();\n    lp.type = \"lowpass\";\n    lp.frequency.value = 700;\n    lp.connect(master);\n    lp.connect(analyser);\n\n    const oscs = [110, 164.81, 220, 277.18].map((f, i) => {\n      const o = ctx.createOscillator();\n      o.type = \"sawtooth\";\n      o.frequency.value = f;\n      o.detune.value = (i - 1.5) * 4;\n      const g = ctx.createGain();\n      g.gain.value = 0.06;\n      o.connect(g).connect(lp);\n      o.start();\n      return o;\n    });\n\n    // tremolo LFO so the energy (and visuals) pulse rhythmically\n    const lfo = ctx.createOscillator();\n    lfo.frequency.value = 1.7;\n    const lfoGain = ctx.createGain();\n    lfoGain.gain.value = 0.22;\n    lfo.connect(lfoGain).connect(master.gain);\n    lfo.start();\n\n    master.gain.linearRampToValueAtTime(0.42, ctx.currentTime + 1.6);\n    ctx.resume?.();\n\n    let raf = 0;\n    const data = new Uint8Array(analyser.frequencyBinCount);\n    const tick = () => {\n      analyser.getByteFrequencyData(data);\n      let s = 0;\n      for (let i = 0; i < data.length; i++) s += data[i];\n      const v = s / data.length / 255;\n      energy.current = (energy.current ?? 0) * 0.7 + v * 0.3;\n      raf = requestAnimationFrame(tick);\n    };\n    tick();\n\n    return () => {\n      cancelAnimationFrame(raf);\n      oscs.forEach((o) => o.stop());\n      lfo.stop();\n      ctx.close();\n      energy.current = 0;\n    };\n  }, [enabled, energy]);\n  return null;\n}\n\nexport function FlyThrough({ count = 460, size = 150 }: { count?: number; size?: number }) {\n  const [reduce, setReduce] = useState(false);\n  const [score, setScore] = useState(0);\n  const [sound, setSound] = useState(false);\n  const energy = useRef(0);\n  const controls = useRef<Controls>({ keys: new Set(), throttle: 16, boost: false });\n  const q = useDeviceTier();\n  const [low, setLow] = useState(false);\n  const n = Math.max(140, Math.round(count * q.scale));\n  const bloomOn = q.bloom && !low;\n\n  useEffect(() => {\n    setReduce(window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches);\n    const c = controls.current;\n    const down = (e: KeyboardEvent) => c.keys.add(e.key.toLowerCase());\n    const up = (e: KeyboardEvent) => c.keys.delete(e.key.toLowerCase());\n    const pDown = () => (c.boost = true);\n    const pUp = () => (c.boost = false);\n    const wheel = (e: WheelEvent) => {\n      c.throttle = THREE.MathUtils.clamp(c.throttle - e.deltaY * 0.02, 4, 70);\n    };\n    window.addEventListener(\"keydown\", down);\n    window.addEventListener(\"keyup\", up);\n    window.addEventListener(\"pointerdown\", pDown);\n    window.addEventListener(\"pointerup\", pUp);\n    window.addEventListener(\"pointercancel\", pUp);\n    window.addEventListener(\"wheel\", wheel, { passive: true });\n    return () => {\n      window.removeEventListener(\"keydown\", down);\n      window.removeEventListener(\"keyup\", up);\n      window.removeEventListener(\"pointerdown\", pDown);\n      window.removeEventListener(\"pointerup\", pUp);\n      window.removeEventListener(\"pointercancel\", pUp);\n      window.removeEventListener(\"wheel\", wheel);\n    };\n  }, []);\n\n  const reach = useCallback(() => setScore((s) => s + 1), []);\n\n  return (\n    <div className=\"relative h-full w-full\" style={{ touchAction: \"none\" }}>\n      <Canvas\n        dpr={q.dpr}\n        gl={{ antialias: false, powerPreference: \"high-performance\" }}\n        camera={{ position: [0, 0, 0], fov: 56, near: 0.1, far: 200 }}\n      >\n        <color attach=\"background\" args={[\"#07070c\"]} />\n        <fog attach=\"fog\" args={[\"#07070c\", 12, 78]} />\n        <CrystalField count={n} size={size} reduce={reduce} energy={energy} />\n        <Target onReach={reach} />\n        <Pilot reduce={reduce} controls={controls} />\n        <AudioReactor enabled={sound} energy={energy} />\n        <AutoDpr onLow={() => setLow(true)} />\n        <PauseWhenOffscreen />\n        {bloomOn && (\n          <EffectComposer>\n            <Bloom intensity={0.95} luminanceThreshold={0.3} luminanceSmoothing={0.3} mipmapBlur radius={0.7} />\n          </EffectComposer>\n        )}\n      </Canvas>\n\n      {/* HUD */}\n      <div className=\"pointer-events-none absolute inset-0 z-10 flex flex-col justify-between p-6\">\n        <div className=\"flex items-start justify-end gap-3\">\n          <div className=\"rounded-full border border-line bg-obsidian/50 px-4 py-2 text-sm text-mist backdrop-blur\">\n            Targets <span className=\"font-semibold text-cyan\">{score}</span>\n          </div>\n          {!reduce && (\n            <button\n              onClick={() => setSound((s) => !s)}\n              className=\"pointer-events-auto rounded-full border border-line bg-obsidian/50 px-4 py-2 text-sm text-mist backdrop-blur transition-colors hover:text-ink\"\n            >\n              {sound ? \"♪ Sound on\" : \"♪ Sound off\"}\n            </button>\n          )}\n        </div>\n        <div className=\"mx-auto rounded-full border border-line bg-obsidian/50 px-4 py-2 text-center text-xs text-mist backdrop-blur\">\n          Mouse or drag to steer · W/S or scroll to throttle · A/D roll · hold to boost · chase the marker\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/prism/FlyThrough.tsx"
    }
  ]
}