{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "splat-scene",
  "type": "registry:component",
  "title": "SplatScene",
  "description": "A real-time Gaussian-splat capture rendered in R3F via World Labs' Spark — photoreal 3D content (the kind AI tools like Marble export) running live in the browser.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei",
    "@sparkjsdev/spark"
  ],
  "registryDependencies": [],
  "tier": "pro",
  "note": "No asset ships with this primitive — self-host a .spz/.ply/.splat under /public and pass it via the `url` prop (e.g. <SplatScene url=\"/splats/yours.spz\" />). Needs WebGL2; render via next/dynamic ssr:false.",
  "files": [
    {
      "path": "components/prism/SplatScene.tsx",
      "content": "\"use client\";\n\nimport { Component, useEffect, useRef, useState } from \"react\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimport { OrbitControls } from \"@react-three/drei\";\nimport { SparkRenderer, SplatMesh } from \"@sparkjsdev/spark\";\n\n/**\n * SplatScene — a real-time Gaussian-splat capture rendered in R3F via World Labs'\n * Spark (the maintained successor to the archived Luma viewer). Photoreal 3D\n * content (the kind AI tools like Marble export) running live in the browser.\n *\n * Built imperatively to match Spark's official vanilla-three example exactly:\n * a SparkRenderer + a SplatMesh added to the scene as siblings. Self-contained\n * fallback, `url` prop, honors prefers-reduced-motion.\n */\nfunction Splats({ url, reduce }: { url: string; reduce: boolean }) {\n  const scene = useThree((s) => s.scene);\n  const gl = useThree((s) => s.gl);\n  const meshRef = useRef<SplatMesh | null>(null);\n\n  useEffect(() => {\n    const spark = new SparkRenderer({ renderer: gl });\n    scene.add(spark);\n\n    const mesh = new SplatMesh({ url });\n    // capture is recorded upside-down — quaternion (1,0,0,0) = 180° about X\n    mesh.quaternion.set(1, 0, 0, 0);\n    mesh.position.set(0, 0, 0);\n    scene.add(mesh);\n    meshRef.current = mesh;\n\n    return () => {\n      scene.remove(mesh);\n      scene.remove(spark);\n      meshRef.current = null;\n    };\n  }, [url, scene, gl]);\n\n  useFrame((_, dt) => {\n    if (meshRef.current && !reduce) meshRef.current.rotation.y += 0.35 * dt;\n  });\n\n  return null;\n}\n\nfunction SplatFallback({ label }: { label: string }) {\n  return (\n    <div\n      className=\"relative grid h-full w-full place-items-center overflow-hidden rounded-2xl border border-line\"\n      style={{ background: \"radial-gradient(120% 90% at 70% 10%, rgba(139,92,255,0.18), transparent 60%), #0c0c13\" }}\n    >\n      <span className=\"relative z-10 text-sm text-muted\">{label}</span>\n    </div>\n  );\n}\n\n/** Catches a failed splat load (e.g. missing asset) and shows the fallback. */\nclass CanvasBoundary extends Component<\n  { fallback: React.ReactNode; children: React.ReactNode },\n  { failed: boolean }\n> {\n  state = { failed: false };\n  static getDerivedStateFromError() {\n    return { failed: true };\n  }\n  render() {\n    return this.state.failed ? this.props.fallback : this.props.children;\n  }\n}\n\nexport function SplatScene({ url = \"/assets/splats/butterfly.spz\" }: { url?: string }) {\n  const [supported, setSupported] = useState<boolean | null>(null);\n  const [reduce, setReduce] = useState(false);\n\n  useEffect(() => {\n    const c = document.createElement(\"canvas\");\n    setSupported(!!c.getContext(\"webgl2\"));\n    setReduce(window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches);\n  }, []);\n\n  if (supported === false) return <SplatFallback label=\"Gaussian splat needs WebGL2\" />;\n  if (supported === null) return <SplatFallback label=\"Loading splat…\" />;\n\n  return (\n    <CanvasBoundary fallback={<SplatFallback label=\"Splat asset unavailable\" />}>\n      <Canvas\n        dpr={[1, 1.5]}\n        gl={{ antialias: false, powerPreference: \"high-performance\" }}\n        camera={{ position: [0, 0, 3], fov: 50 }}\n      >\n        <Splats url={url} reduce={reduce} />\n        <OrbitControls enablePan={false} enableZoom={false} autoRotate={!reduce} autoRotateSpeed={0.5} />\n      </Canvas>\n    </CanvasBoundary>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/prism/SplatScene.tsx"
    }
  ]
}