{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "canvas-perf",
  "type": "registry:component",
  "title": "Canvas performance helpers",
  "description": "Make any R3F scene smooth on any device: useDeviceTier (device heuristic for a starting quality), AutoDpr (PerformanceMonitor + AdaptiveDpr — holds framerate by trading resolution), and PauseWhenOffscreen (stop rendering when off-screen).",
  "dependencies": [
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "registryDependencies": [],
  "tier": "free",
  "note": "AutoDpr + PauseWhenOffscreen render INSIDE a <Canvas>; useDeviceTier is a plain hook the wrapper uses to pick a starting dpr/count/bloom.",
  "files": [
    {
      "path": "lib/useDeviceTier.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * useDeviceTier — a cheap, synchronous device-capability heuristic that picks a\n * *starting* quality for WebGL scenes. Runtime adaptation (PerformanceMonitor +\n * AdaptiveDpr) then corrects from here, so a wrong guess self-heals — this just\n * avoids opening a heavy device at full quality and stuttering on the first frames.\n *\n * SSR-safe: returns the \"high\" default on the server / first render, then resolves\n * the real tier on mount.\n */\nexport type Quality = {\n  tier: \"low\" | \"mid\" | \"high\";\n  /** [min, max] DPR range — AdaptiveDpr scales within it by measured FPS. */\n  dpr: [number, number];\n  /** whether to render the (expensive) bloom pass at all. */\n  bloom: boolean;\n  /** instance/particle count multiplier. */\n  scale: number;\n};\n\nconst HIGH: Quality = { tier: \"high\", dpr: [0.8, 1.5], bloom: true, scale: 1 };\nconst MID: Quality = { tier: \"mid\", dpr: [0.7, 1.3], bloom: true, scale: 0.75 };\nconst LOW: Quality = { tier: \"low\", dpr: [0.6, 1], bloom: false, scale: 0.5 };\n\nexport function useDeviceTier(): Quality {\n  const [q, setQ] = useState<Quality>(HIGH);\n\n  useEffect(() => {\n    const nav = navigator as Navigator & { deviceMemory?: number };\n    const coarse = window.matchMedia(\"(pointer: coarse)\").matches; // phones/tablets\n    const cores = nav.hardwareConcurrency ?? 8;\n    const mem = nav.deviceMemory ?? 8;\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let base: Quality;\n    if (coarse || cores <= 4 || mem <= 4) base = LOW;\n    else if (cores <= 6 || mem <= 6) base = MID;\n    else base = HIGH;\n\n    // reduced-motion users get the static path anyway; keep counts modest.\n    setQ(reduce ? { ...base, scale: Math.min(base.scale, 0.6) } : base);\n  }, []);\n\n  return q;\n}\n",
      "type": "registry:lib",
      "target": "lib/useDeviceTier.ts"
    },
    {
      "path": "components/showcase/CanvasPerf.tsx",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useThree } from \"@react-three/fiber\";\nimport { PerformanceMonitor, AdaptiveDpr } from \"@react-three/drei\";\n\n/**\n * AutoDpr — drop inside any <Canvas>. PerformanceMonitor watches the real\n * framerate and AdaptiveDpr trades resolution to hold it steady (within the\n * Canvas's dpr range). If the device keeps struggling, onLow fires once so the\n * scene can shed its heaviest pass (bloom). This is the core \"smooth on anything\"\n * mechanism — it self-corrects no matter the device.\n */\nexport function AutoDpr({ onLow }: { onLow?: () => void }) {\n  return (\n    <>\n      <PerformanceMonitor flipflops={3} onFallback={() => onLow?.()} />\n      <AdaptiveDpr />\n    </>\n  );\n}\n\n/**\n * PauseWhenOffscreen — stops the render loop when the canvas leaves the viewport\n * and resumes it on return. Kills wasted GPU/CPU from sticky or below-fold\n * canvases that stay mounted while you're looking elsewhere.\n */\nexport function PauseWhenOffscreen() {\n  const gl = useThree((s) => s.gl);\n  const setFrameloop = useThree((s) => s.setFrameloop);\n\n  useEffect(() => {\n    const el = gl.domElement;\n    // Lenient margin so we only pause when clearly off-screen; IntersectionObserver\n    // also re-fires when the canvas resizes, so a briefly-mismeasured canvas resumes.\n    const io = new IntersectionObserver(\n      ([entry]) => setFrameloop(entry.isIntersecting ? \"always\" : \"never\"),\n      { rootMargin: \"200px\" }\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [gl, setFrameloop]);\n\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/showcase/CanvasPerf.tsx"
    }
  ]
}