{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "debug-canvas",
  "type": "registry:component",
  "title": "r3f Canvas",
  "description": "An enhanced R3F Canvas with built-in debug helpers — orbit/fly controls (mutually exclusive), grid, camera monitor, and performance stats — all toggled via a shared tweakpane folder.",
  "dependencies": [
    "stats-gl",
    "@react-three/drei"
  ],
  "registryDependencies": [
    "@joyco/debug"
  ],
  "files": [
    {
      "path": "registry/components/debug-canvas/index.ts",
      "content": "export { DebugCanvas } from './debug-canvas'\nexport { DebugControls } from './debug-controls'\nexport { DebugGridHelper } from './debug-grid-helper'\nexport { DebugCameraMonitor } from './debug-camera-monitor'\nexport { DebugPerfMonitor } from './debug-perf-monitor'\n",
      "type": "registry:component"
    },
    {
      "path": "registry/components/debug-canvas/debug-canvas.tsx",
      "content": "'use client'\n\nimport { Canvas, type CanvasProps } from '@react-three/fiber'\nimport { type ReactNode } from 'react'\nimport { DebugControls } from './debug-controls'\nimport { DebugGridHelper } from './debug-grid-helper'\nimport { DebugCameraMonitor } from './debug-camera-monitor'\nimport { DebugPerfMonitor } from './debug-perf-monitor'\n\ninterface DebugCanvasProps extends CanvasProps {\n  children?: ReactNode\n}\n\nexport function DebugCanvas({ children, ...props }: DebugCanvasProps) {\n  return (\n    <Canvas {...props}>\n      <DebugControls />\n      <DebugGridHelper />\n      <DebugCameraMonitor />\n      <DebugPerfMonitor />\n      {children}\n    </Canvas>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/components/debug-canvas/debug-controls.tsx",
      "content": "'use client'\n/* eslint-disable react-hooks/immutability */\n\nimport { OrbitControls } from '@react-three/drei'\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { useEffect, useRef, useState } from 'react'\nimport { Euler, Vector3 } from 'three'\nimport { useDebugBindings, useDebugState } from '@/registry/lib/debug'\n\nfunction ActiveOrbitControls() {\n  const camera = useThree((s) => s.camera)\n  const gl = useThree((s) => s.gl)\n\n  const [target] = useState(() => {\n    if (camera.userData.target instanceof Vector3) {\n      return new Vector3().copy(camera.userData.target)\n    }\n    return new Vector3(0, 0, 0)\n  })\n\n  useEffect(() => {\n    const canvas = gl.domElement\n    canvas.style.pointerEvents = 'auto'\n    return () => {\n      canvas.style.pointerEvents = ''\n    }\n  }, [gl])\n\n  return <OrbitControls target={target} makeDefault />\n}\n\nconst HALF_PI = Math.PI / 2\nconst _forward = new Vector3()\nconst _right = new Vector3()\n\nfunction ActiveFlyControls() {\n  const camera = useThree((s) => s.camera)\n  const gl = useThree((s) => s.gl)\n  const yawRef = useRef(0)\n  const pitchRef = useRef(0)\n  const keysRef = useRef({\n    w: false,\n    a: false,\n    s: false,\n    d: false,\n    space: false,\n    shift: false,\n  })\n  const speed = 3\n  const sprintMultiplier = 5\n  const sensitivity = 0.002\n\n  useEffect(() => {\n    const euler = new Euler()\n    euler.setFromQuaternion(camera.quaternion, 'YXZ')\n    yawRef.current = euler.y\n    pitchRef.current = euler.x\n  }, [camera])\n\n  useEffect(() => {\n    const canvas = gl.domElement\n    canvas.style.pointerEvents = 'auto'\n\n    const onClick = () => canvas.requestPointerLock()\n\n    const onMouseMove = (e: MouseEvent) => {\n      if (document.pointerLockElement !== canvas) return\n      yawRef.current -= e.movementX * sensitivity\n      pitchRef.current -= e.movementY * sensitivity\n      pitchRef.current = Math.max(-HALF_PI, Math.min(HALF_PI, pitchRef.current))\n    }\n\n    canvas.addEventListener('click', onClick)\n    document.addEventListener('mousemove', onMouseMove)\n\n    return () => {\n      canvas.removeEventListener('click', onClick)\n      document.removeEventListener('mousemove', onMouseMove)\n      if (document.pointerLockElement === canvas) document.exitPointerLock()\n      canvas.style.pointerEvents = ''\n    }\n  }, [gl])\n\n  useEffect(() => {\n    const keys = keysRef.current\n\n    const set = (e: KeyboardEvent, pressed: boolean) => {\n      switch (e.code) {\n        case 'KeyW':\n          keys.w = pressed\n          break\n        case 'KeyA':\n          keys.a = pressed\n          break\n        case 'KeyS':\n          keys.s = pressed\n          break\n        case 'KeyD':\n          keys.d = pressed\n          break\n        case 'Space':\n          e.preventDefault()\n          keys.space = pressed\n          break\n        case 'ShiftLeft':\n        case 'ShiftRight':\n          keys.shift = pressed\n          break\n      }\n    }\n\n    const onDown = (e: KeyboardEvent) => set(e, true)\n    const onUp = (e: KeyboardEvent) => set(e, false)\n\n    window.addEventListener('keydown', onDown)\n    window.addEventListener('keyup', onUp)\n    return () => {\n      window.removeEventListener('keydown', onDown)\n      window.removeEventListener('keyup', onUp)\n    }\n  }, [])\n\n  useFrame((_, delta) => {\n    const keys = keysRef.current\n\n    camera.rotation.set(pitchRef.current, yawRef.current, 0, 'YXZ')\n\n    camera.getWorldDirection(_forward)\n    _right.crossVectors(_forward, camera.up).normalize()\n\n    const step = speed * (keys.shift ? sprintMultiplier : 1) * delta\n    if (keys.w) camera.position.addScaledVector(_forward, step)\n    if (keys.s) camera.position.addScaledVector(_forward, -step)\n    if (keys.a) camera.position.addScaledVector(_right, -step)\n    if (keys.d) camera.position.addScaledVector(_right, step)\n    if (keys.space) camera.position.y += step\n  })\n\n  return null\n}\n\nexport function DebugControls() {\n  const [targetRef, , store] = useDebugBindings('Canvas', {\n    orbitControls: true,\n    flyControls: false,\n  })\n\n  // Mutual exclusion: enabling one disables the other\n  useEffect(() => {\n    return store.subscribe((state, prev) => {\n      if (state.orbitControls && !prev.orbitControls && state.flyControls) {\n        targetRef.current.flyControls = false\n      } else if (\n        state.flyControls &&\n        !prev.flyControls &&\n        state.orbitControls\n      ) {\n        targetRef.current.orbitControls = false\n      }\n    })\n  }, [store, targetRef])\n\n  // Alt+F keyboard toggle for fly controls\n  useEffect(() => {\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.altKey && e.code === 'KeyF') {\n        e.preventDefault()\n        targetRef.current.flyControls = !targetRef.current.flyControls\n      }\n    }\n    window.addEventListener('keydown', onKeyDown)\n    return () => window.removeEventListener('keydown', onKeyDown)\n  }, [targetRef])\n\n  const orbit = useDebugState(store, (s) => s.orbitControls)\n  const fly = useDebugState(store, (s) => s.flyControls)\n\n  return (\n    <>\n      {orbit && <ActiveOrbitControls />}\n      {fly && <ActiveFlyControls />}\n    </>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/components/debug-canvas/debug-grid-helper.tsx",
      "content": "'use client'\n\nimport { useDebugBindings, useDebugState } from '@/registry/lib/debug'\n\nexport function DebugGridHelper() {\n  const [, , store] = useDebugBindings('Canvas', {\n    gridHelper: true,\n  })\n  const gridHelperEnabled = useDebugState(store, (s) => s.gridHelper)\n\n  if (!gridHelperEnabled) return null\n\n  return <gridHelper args={[100, 100]} />\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/components/debug-canvas/debug-camera-monitor.tsx",
      "content": "'use client'\n\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { useEffect, useRef } from 'react'\nimport { Euler } from 'three'\nimport { useDebugFolder } from '@/registry/lib/debug'\n\nconst RAD2DEG = 180 / Math.PI\nconst _euler = new Euler()\n\nexport function DebugCameraMonitor() {\n  const folder = useDebugFolder('Canvas')\n  const camera = useThree((s) => s.camera)\n  const paramsRef = useRef({\n    position: { x: 0, y: 0, z: 0 },\n    rotation: { x: 0, y: 0, z: 0 },\n  })\n  const bindingsRef = useRef<{ dispose(): void }[]>([])\n\n  useEffect(() => {\n    if (!folder) return\n\n    const fmt = (v: number) => v.toFixed(2)\n    const pos = folder.addBinding(paramsRef.current, 'position', {\n      label: 'cam pos',\n      x: { format: fmt },\n      y: { format: fmt },\n      z: { format: fmt },\n    })\n    const rot = folder.addBinding(paramsRef.current, 'rotation', {\n      label: 'cam rot',\n      x: { format: fmt },\n      y: { format: fmt },\n      z: { format: fmt },\n    })\n\n    bindingsRef.current = [pos, rot]\n\n    return () => {\n      pos.dispose()\n      rot.dispose()\n      bindingsRef.current = []\n    }\n  }, [folder])\n\n  useFrame(() => {\n    const p = paramsRef.current.position\n    const r = paramsRef.current.rotation\n\n    p.x = camera.position.x\n    p.y = camera.position.y\n    p.z = camera.position.z\n\n    _euler.setFromQuaternion(camera.quaternion, 'YXZ')\n    r.x = _euler.x * RAD2DEG\n    r.y = _euler.y * RAD2DEG\n    r.z = _euler.z * RAD2DEG\n\n    for (const b of bindingsRef.current) {\n      ;(b as unknown as { refresh(): void }).refresh()\n    }\n  })\n\n  return null\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/components/debug-canvas/debug-perf-monitor.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { useDebugBindings, useDebugState } from '@/registry/lib/debug'\nimport Stats from 'stats-gl'\nimport { useFrame, useThree } from '@react-three/fiber'\n\ntype Panel = InstanceType<typeof Stats.Panel>\n\nexport function DebugPerfMonitor() {\n  const [, , store] = useDebugBindings('Canvas', { perfMonitor: false })\n  const enabled = useDebugState(store, (s) => s.perfMonitor)\n\n  if (!enabled) return null\n\n  return <PerfMonitorGL />\n}\n\nfunction PerfMonitorGL() {\n  const gl = useThree((s) => s.gl)\n  const statsRef = useRef<Stats | null>(null)\n  const trianglesPanelRef = useRef<Panel | null>(null)\n  const callsPanelRef = useRef<Panel | null>(null)\n  const maxTrianglesRef = useRef(0)\n  const maxCallsRef = useRef(0)\n  const prevTrianglesRef = useRef(0)\n  const prevCallsRef = useRef(0)\n\n  useEffect(() => {\n    const stats = new Stats({\n      trackHz: true,\n      trackFPS: true,\n    })\n\n    let cancelled = false\n\n    const setup = async () => {\n      await stats.init(gl)\n      if (cancelled) return\n\n      const trianglesPanel = stats.addPanel(new Stats.Panel('TRI', '#f80', '#210'))\n      const callsPanel = stats.addPanel(new Stats.Panel('CALL', '#e08', '#201'))\n\n      stats.dom.style.cssText = `\n        position: fixed;\n        bottom: 0;\n        right: 0;\n        z-index: 10000;\n        opacity: 0.9;\n        display: flex;\n        pointer-events: auto;\n      `\n\n      const vsyncCanvas = (stats as unknown as { vsyncPanel?: { canvas: HTMLElement } }).vsyncPanel?.canvas\n\n      const applyFlexLayout = () => {\n        for (const child of Array.from(stats.dom.children) as HTMLElement[]) {\n          if (child === vsyncCanvas) continue\n          child.style.position = 'relative'\n          child.style.top = 'auto'\n          child.style.left = 'auto'\n        }\n      }\n\n      applyFlexLayout()\n      window.addEventListener('resize', applyFlexLayout)\n\n      document.body.appendChild(stats.dom)\n      statsRef.current = stats\n      trianglesPanelRef.current = trianglesPanel\n      callsPanelRef.current = callsPanel\n\n      return applyFlexLayout\n    }\n\n    const cleanupP = setup()\n\n    return () => {\n      cancelled = true\n      statsRef.current = null\n      trianglesPanelRef.current = null\n      callsPanelRef.current = null\n      cleanupP.then((applyFlexLayout) => {\n        if (applyFlexLayout) window.removeEventListener('resize', applyFlexLayout)\n        stats.dom.remove()\n        stats.dispose()\n      }).catch(() => {\n        // init failed — nothing was appended, nothing to clean up\n      })\n    }\n  }, [gl])\n\n  useFrame(() => {\n    const info = gl.info\n    prevTrianglesRef.current = info?.render?.triangles ?? 0\n    prevCallsRef.current = info?.render?.calls ?? 0\n\n    statsRef.current?.begin()\n  }, -Infinity)\n\n  useFrame(() => {\n    const info = gl.info\n    const triangles = (info?.render?.triangles ?? 0) - prevTrianglesRef.current\n    const calls = (info?.render?.calls ?? 0) - prevCallsRef.current\n\n    maxTrianglesRef.current = Math.max(maxTrianglesRef.current, triangles)\n    maxCallsRef.current = Math.max(maxCallsRef.current, calls)\n\n    trianglesPanelRef.current?.update(triangles, maxTrianglesRef.current)\n    trianglesPanelRef.current?.updateGraph(triangles, maxTrianglesRef.current)\n\n    callsPanelRef.current?.update(calls, maxCallsRef.current)\n    callsPanelRef.current?.updateGraph(calls, maxCallsRef.current)\n\n    statsRef.current?.end()\n    statsRef.current?.update()\n  }, Infinity)\n\n  return null\n}\n",
      "type": "registry:component"
    }
  ]
}