{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "depth-scene",
  "type": "registry:component",
  "title": "DepthScene",
  "description": "On-device WebGPU AI: estimates depth from a flat photo with Depth-Anything-V2 (in-browser via Transformers.js), then displaces a plane for cursor-steered 3D parallax. WebGPU → WASM → flat fallback.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@huggingface/transformers"
  ],
  "registryDependencies": [],
  "tier": "pro",
  "note": "Lazy-loads the model in a Web Worker on click. Ships NO image — pass your own self-hosted photo via the `image` prop (defaults to /demo/scene.jpg); the visual falls back to that flat image if on-device AI is unavailable. Render via next/dynamic ssr:false.",
  "files": [
    {
      "path": "components/prism/DepthScene.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport * as THREE from \"three\";\n\n/**\n * DepthScene — on-device WebGPU AI: estimates a depth map from a flat photo with\n * Depth-Anything-V2 (Apache-2.0) running entirely in the browser via\n * Transformers.js, then uses it as a displacement map on a subdivided plane for a\n * real 3D parallax you steer with your cursor. Model loads in a Web Worker only\n * on click. Graceful ladder: WebGPU → WASM → flat image (the visual never fails).\n *\n * Pass your own self-hosted photo via `image`. Honors prefers-reduced-motion\n * (no pointer parallax). Frontier flex: zero-backend, in-browser ML → a 3D primitive.\n */\nconst DEFAULT_IMAGE = \"/demo/scene.jpg\";\n\nfunction ParallaxPlane({\n  color,\n  depth,\n  aspect,\n  reduce,\n}: {\n  color: THREE.Texture;\n  depth: THREE.Texture;\n  aspect: number;\n  reduce: boolean;\n}) {\n  const mouse = useRef({ x: 0, y: 0 });\n  useEffect(() => {\n    if (reduce) return;\n    const onMove = (e: PointerEvent) => {\n      mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1;\n      mouse.current.y = -(e.clientY / window.innerHeight) * 2 + 1;\n    };\n    window.addEventListener(\"pointermove\", onMove);\n    return () => window.removeEventListener(\"pointermove\", onMove);\n  }, [reduce]);\n  useFrame((state) => {\n    if (reduce) return;\n    state.camera.position.x += (mouse.current.x * 0.4 - state.camera.position.x) * 0.05;\n    state.camera.position.y += (mouse.current.y * 0.4 - state.camera.position.y) * 0.05;\n    state.camera.lookAt(0, 0, 0);\n  });\n  return (\n    <mesh>\n      <planeGeometry args={[3, 3 / aspect, 200, 200]} />\n      <meshStandardMaterial\n        map={color}\n        displacementMap={depth}\n        displacementScale={0.6}\n        roughness={0.9}\n      />\n    </mesh>\n  );\n}\n\ntype Status = \"idle\" | \"loading\" | \"ready\" | \"flat\";\n\nexport function DepthScene({ image = DEFAULT_IMAGE }: { image?: string }) {\n  const workerRef = useRef<Worker | null>(null);\n  const [status, setStatus] = useState<Status>(\"idle\");\n  const [progress, setProgress] = useState(0);\n  const [reduce, setReduce] = useState(false);\n  const [tex, setTex] = useState<{ color: THREE.Texture; depth: THREE.Texture; aspect: number } | null>(\n    null\n  );\n\n  useEffect(() => {\n    setReduce(window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches);\n  }, []);\n\n  const run = useCallback(async () => {\n    setStatus(\"loading\");\n    const nav = navigator as Navigator & { gpu?: { requestAdapter: () => Promise<unknown> } };\n    let preferWebGPU = false;\n    if (nav.gpu) {\n      try {\n        preferWebGPU = !!(await nav.gpu.requestAdapter());\n      } catch {\n        preferWebGPU = false;\n      }\n    }\n\n    const worker = (workerRef.current ??= new Worker(\n      new URL(\"./depth.worker.ts\", import.meta.url),\n      { type: \"module\" }\n    ));\n\n    worker.onmessage = (e: MessageEvent) => {\n      const m = e.data;\n      if (m.type === \"progress\") {\n        // only advance on numeric percents; the worker already filters non-progress events\n        if (typeof m.p?.progress === \"number\") setProgress(m.p.progress);\n      } else if (m.type === \"error\") {\n        setStatus(\"flat\");\n      } else if (m.type === \"done\") {\n        const { width, height, data } = m as { width: number; height: number; data: Uint8Array };\n        const depthTex = new THREE.DataTexture(data, width, height, THREE.RedFormat);\n        // DataTexture defaults flipY=false; the color photo (TextureLoader) defaults\n        // flipY=true — match them so the relief isn't mirrored vertically.\n        depthTex.flipY = true;\n        depthTex.needsUpdate = true;\n        const colorTex = new THREE.TextureLoader().load(image, () => {\n          colorTex.colorSpace = THREE.SRGBColorSpace;\n          setTex({ color: colorTex, depth: depthTex, aspect: width / height });\n          setStatus(\"ready\"); // gate the Canvas on ready (color image decoded)\n        });\n      }\n    };\n    worker.postMessage({ url: image, preferWebGPU });\n  }, [image]);\n\n  useEffect(() => () => workerRef.current?.terminate(), []);\n\n  return (\n    <div className=\"relative h-full w-full overflow-hidden rounded-2xl border border-line bg-obsidian-2\">\n      {status !== \"ready\" && (\n        // eslint-disable-next-line @next/next/no-img-element\n        <img src={image} alt=\"\" className=\"absolute inset-0 h-full w-full object-cover opacity-70\" />\n      )}\n      {status === \"idle\" && (\n        <button\n          onClick={run}\n          aria-busy={false}\n          className=\"absolute inset-0 z-10 grid place-items-center\"\n        >\n          <span className=\"rounded-full bg-ink px-5 py-2.5 text-sm font-semibold text-obsidian transition-transform hover:scale-[1.03]\">\n            Generate depth → enter 3D\n          </span>\n        </button>\n      )}\n      {status === \"loading\" && (\n        <div\n          role=\"status\"\n          aria-live=\"polite\"\n          aria-busy=\"true\"\n          className=\"absolute inset-0 z-10 grid place-items-center bg-obsidian/50 text-sm text-mist backdrop-blur-sm\"\n        >\n          <span>\n            Running model on-device…{\" \"}\n            <span aria-hidden>{Math.round(progress)}%</span>\n          </span>\n        </div>\n      )}\n      {status === \"flat\" && (\n        <div\n          role=\"status\"\n          aria-live=\"polite\"\n          className=\"absolute bottom-3 left-3 z-10 rounded bg-obsidian/70 px-2 py-1 text-xs text-muted backdrop-blur\"\n        >\n          On-device AI unavailable here — showing the flat source image\n        </div>\n      )}\n      {status === \"ready\" && tex && (\n        <Canvas camera={{ position: [0, 0, 2.2], fov: 45 }} dpr={[1, 2]}>\n          <ambientLight intensity={0.85} />\n          <directionalLight position={[2, 2, 3]} intensity={1.1} />\n          <ParallaxPlane color={tex.color} depth={tex.depth} aspect={tex.aspect} reduce={reduce} />\n        </Canvas>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/prism/DepthScene.tsx"
    },
    {
      "path": "components/prism/depth.worker.ts",
      "content": "// Web Worker: runs monocular depth estimation fully on-device.\n// Device ladder: WebGPU fp16 → WebGPU fp32 → WASM q8. Loaded only when the main\n// thread posts a message (i.e. on user click), so zero AI bytes on page load.\nimport { pipeline, env } from \"@huggingface/transformers\";\n\nenv.allowLocalModels = false; // pull ONNX weights from the HF CDN\n\nconst MODEL = \"onnx-community/depth-anything-v2-small\"; // Apache-2.0\nconst LADDER: { device: \"webgpu\" | \"wasm\"; dtype: \"fp16\" | \"fp32\" | \"q8\" }[] = [\n  { device: \"webgpu\", dtype: \"fp16\" },\n  { device: \"webgpu\", dtype: \"fp32\" },\n  { device: \"wasm\", dtype: \"q8\" },\n];\n\ntype Estimator = (\n  input: string\n) => Promise<{ depth: { data: Uint8Array; width: number; height: number } }>;\n\n// Typed message port without pulling in the webworker lib (keeps tsc on dom lib).\nconst port = globalThis as unknown as {\n  postMessage: (message: unknown, transfer?: Transferable[]) => void;\n  onmessage: ((e: MessageEvent) => void) | null;\n};\n\nlet estimator: Estimator | null = null;\n\nasync function getEstimator(preferWebGPU: boolean): Promise<Estimator> {\n  if (estimator) return estimator;\n  let lastErr: unknown;\n  for (let i = preferWebGPU ? 0 : 2; i < LADDER.length; i++) {\n    const cfg = LADDER[i];\n    try {\n      port.postMessage({ type: \"status\", msg: `loading (${cfg.device}/${cfg.dtype})` });\n      estimator = (await pipeline(\"depth-estimation\", MODEL, {\n        device: cfg.device,\n        dtype: cfg.dtype,\n        // forward only events that carry a numeric percent, so the bar doesn't\n        // snap back to 0 on non-download statuses (initiate/done/ready).\n        progress_callback: (p: unknown) => {\n          if (typeof (p as { progress?: number }).progress === \"number\") {\n            port.postMessage({ type: \"progress\", p });\n          }\n        },\n      })) as unknown as Estimator;\n      return estimator;\n    } catch (e) {\n      lastErr = e;\n      estimator = null;\n    }\n  }\n  throw lastErr ?? new Error(\"no backend available\");\n}\n\nport.onmessage = async (e: MessageEvent) => {\n  const { url, preferWebGPU } = e.data as { url: string; preferWebGPU: boolean };\n  try {\n    const est = await getEstimator(preferWebGPU);\n    const out = await est(url);\n    const depth = out.depth;\n    port.postMessage(\n      { type: \"done\", width: depth.width, height: depth.height, data: depth.data },\n      [depth.data.buffer]\n    );\n  } catch (err) {\n    port.postMessage({ type: \"error\", error: String(err) });\n  }\n};\n",
      "type": "registry:file",
      "target": "components/prism/depth.worker.ts"
    }
  ]
}