{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"model-studio","type":"registry:ui","title":"Model Studio","description":"Three.js studio viewer for a procedural model: physical clearcoat material, soft shadows, drag to orbit, auto-rotate and clamped zoom.","dependencies":["three"],"registryDependencies":[],"files":[{"path":"registry/ui/model-studio.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport { RoomEnvironment } from \"three/examples/jsm/environments/RoomEnvironment.js\";\nimport { RoundedBoxGeometry } from \"three/examples/jsm/geometries/RoundedBoxGeometry.js\";\n\nexport interface ModelStudioProps {\n  /** Procedural model to show. */\n  shape?: \"torus-knot\" | \"blob\" | \"rounded-box\" | \"torus\";\n  color?: string;\n  /** 0 = dielectric, 1 = metal. */\n  metalness?: number;\n  roughness?: number;\n  /** Thin-film rainbow sheen, 0 to 1. */\n  iridescence?: number;\n  autoRotate?: boolean;\n  /** Auto-rotate speed (OrbitControls units, 2 = 30s per turn). */\n  autoRotateSpeed?: number;\n  /** Closest camera distance. */\n  minDistance?: number;\n  /** Farthest camera distance. */\n  maxDistance?: number;\n  /** Soft contact shadow under the model. */\n  shadow?: boolean;\n  className?: string;\n}\n\nfunction makeGeometry(shape: NonNullable<ModelStudioProps[\"shape\"]>) {\n  if (shape === \"torus\") return new THREE.TorusGeometry(0.9, 0.38, 64, 160);\n  if (shape === \"rounded-box\") return new RoundedBoxGeometry(1.5, 1.5, 1.5, 8, 0.28);\n  if (shape === \"blob\") {\n    // Icosphere pushed around by layered sines: an organic pebble.\n    const g = new THREE.IcosahedronGeometry(1.15, 48);\n    const p = g.attributes.position;\n    const v = new THREE.Vector3();\n    for (let i = 0; i < p.count; i++) {\n      v.fromBufferAttribute(p, i);\n      const n = v.clone().normalize();\n      const d = 0.12 * Math.sin(n.x * 3.1 + n.y * 1.7) + 0.08 * Math.sin(n.y * 4.3 - n.z * 2.9) + 0.05 * Math.sin(n.z * 6.1 + n.x * 5.3);\n      v.copy(n).multiplyScalar(1.15 + d);\n      p.setXYZ(i, v.x, v.y, v.z);\n    }\n    g.computeVertexNormals();\n    return g;\n  }\n  return new THREE.TorusKnotGeometry(0.75, 0.28, 320, 48);\n}\n\nexport function ModelStudio({\n  shape = \"torus-knot\",\n  color = \"#c4b5fd\",\n  metalness = 0.65,\n  roughness = 0.18,\n  iridescence = 0.6,\n  autoRotate = true,\n  autoRotateSpeed = 1.5,\n  minDistance = 3,\n  maxDistance = 9,\n  shadow = true,\n  className,\n}: ModelStudioProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const three = useRef<{ material: THREE.MeshPhysicalMaterial; controls: OrbitControls; ground: THREE.Mesh } | null>(null);\n\n  useEffect(() => {\n    const el = host.current!;\n    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n    renderer.toneMapping = THREE.ACESFilmicToneMapping;\n    renderer.shadowMap.enabled = true;\n    renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n    renderer.domElement.style.cssText = \"display:block;width:100%;height:100%;touch-action:none;cursor:grab\";\n    renderer.domElement.setAttribute(\"aria-hidden\", \"true\");\n    el.appendChild(renderer.domElement);\n\n    const scene = new THREE.Scene();\n    const pmrem = new THREE.PMREMGenerator(renderer);\n    const room = new RoomEnvironment();\n    const envTex = pmrem.fromScene(room, 0.04).texture;\n    scene.environment = envTex;\n\n    const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);\n    camera.position.set(3.2, 1.6, 4.4);\n\n    // Studio rig: warm key with soft shadows, cool rim from behind.\n    const key = new THREE.DirectionalLight(0xfff1e0, 2.2);\n    key.position.set(3, 5, 3);\n    key.castShadow = true;\n    key.shadow.mapSize.set(1024, 1024);\n    key.shadow.radius = 8;\n    Object.assign(key.shadow.camera, { left: -3, right: 3, top: 3, bottom: -3 });\n    const rim = new THREE.DirectionalLight(0x9ecbff, 1.6);\n    rim.position.set(-4, 2, -4);\n    scene.add(key, rim, new THREE.AmbientLight(0xffffff, 0.15));\n\n    const material = new THREE.MeshPhysicalMaterial({\n      clearcoat: 1,\n      clearcoatRoughness: 0.08,\n      iridescenceIOR: 1.6,\n      envMapIntensity: 1.2,\n    });\n    const geometry = makeGeometry(shape);\n    const mesh = new THREE.Mesh(geometry, material);\n    mesh.castShadow = true;\n    const model = new THREE.Group();\n    model.add(mesh);\n    scene.add(model);\n\n    const ground = new THREE.Mesh(new THREE.PlaneGeometry(12, 12), new THREE.ShadowMaterial({ opacity: 0.35 }));\n    ground.rotation.x = -Math.PI / 2;\n    ground.position.y = -1.55;\n    ground.receiveShadow = true;\n    scene.add(ground);\n\n    const controls = new OrbitControls(camera, renderer.domElement);\n    controls.enableDamping = true;\n    controls.enablePan = false;\n    controls.minPolarAngle = 0.2;\n    controls.maxPolarAngle = Math.PI / 2 + 0.05;\n    three.current = { material, controls, ground };\n    const grab = () => (renderer.domElement.style.cursor = \"grabbing\");\n    const release = () => (renderer.domElement.style.cursor = \"grab\");\n    controls.addEventListener(\"start\", grab);\n    controls.addEventListener(\"end\", release);\n\n    // Keyboard: arrows orbit the model, +/- zoom.\n    const onKey = (e: KeyboardEvent) => {\n      const step = 0.25;\n      if (e.key === \"ArrowLeft\") model.rotation.y -= step;\n      else if (e.key === \"ArrowRight\") model.rotation.y += step;\n      else if (e.key === \"ArrowUp\") model.rotation.x -= step;\n      else if (e.key === \"ArrowDown\") model.rotation.x += step;\n      else if (e.key === \"+\" || e.key === \"=\" || e.key === \"-\") {\n        const dir = camera.position.clone().sub(controls.target);\n        const len = THREE.MathUtils.clamp(dir.length() * (e.key === \"-\" ? 1.15 : 0.87), controls.minDistance, controls.maxDistance);\n        camera.position.copy(controls.target).add(dir.setLength(len));\n      } else return;\n      e.preventDefault();\n    };\n    el.addEventListener(\"keydown\", onKey);\n\n    const resize = () => {\n      const w = el.clientWidth || 1;\n      const h = el.clientHeight || 1;\n      renderer.setSize(w, h, false);\n      camera.aspect = w / h;\n      camera.updateProjectionMatrix();\n    };\n    const ro = new ResizeObserver(resize);\n    ro.observe(el);\n    resize();\n\n    let raf = 0;\n    const loop = () => {\n      raf = requestAnimationFrame(loop);\n      controls.update();\n      renderer.render(scene, camera);\n    };\n    const io = new IntersectionObserver(([entry]) => {\n      cancelAnimationFrame(raf);\n      if (entry.isIntersecting) loop();\n    });\n    io.observe(el);\n    loop();\n\n    return () => {\n      cancelAnimationFrame(raf);\n      io.disconnect();\n      ro.disconnect();\n      el.removeEventListener(\"keydown\", onKey);\n      controls.removeEventListener(\"start\", grab);\n      controls.removeEventListener(\"end\", release);\n      controls.dispose();\n      geometry.dispose();\n      material.dispose();\n      ground.geometry.dispose();\n      (ground.material as THREE.Material).dispose();\n      envTex.dispose();\n      pmrem.dispose();\n      room.dispose();\n      renderer.dispose();\n      renderer.getContext().getExtension(\"WEBGL_lose_context\")?.loseContext();\n      renderer.domElement.remove();\n      three.current = null;\n    };\n  }, [shape]);\n\n  // Cheap prop changes update the live scene instead of rebuilding the WebGL context.\n  useEffect(() => {\n    const t = three.current;\n    if (!t) return;\n    t.material.color.set(color);\n    Object.assign(t.material, { metalness, roughness, iridescence });\n    t.controls.autoRotate = autoRotate && !matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    t.controls.autoRotateSpeed = autoRotateSpeed;\n    t.controls.minDistance = minDistance;\n    t.controls.maxDistance = Math.max(minDistance, maxDistance);\n    t.ground.visible = shadow;\n  }, [shape, color, metalness, roughness, iridescence, autoRotate, autoRotateSpeed, minDistance, maxDistance, shadow]);\n\n  return (\n    <div\n      ref={host}\n      tabIndex={0}\n      role=\"img\"\n      aria-label=\"3D model viewer. Drag or use arrow keys to rotate, scroll or plus and minus to zoom.\"\n      className={`relative size-full rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring ${className ?? \"\"}`}\n    />\n  );\n}\n"}]}