{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "procedural-product-viewer",
  "type": "registry:ui",
  "title": "3D product viewer",
  "description": "Presents a procedural 3D product with drag inspection, inertia, and recentering.",
  "dependencies": [
    "motion",
    "three"
  ],
  "devDependencies": [
    "@types/three"
  ],
  "categories": [
    "cards-media"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/procedural-product-viewer/",
  "meta": {
    "engines": [
      "motion",
      "three"
    ],
    "runtimeCost": "heavy",
    "signature": "Drag-inspectable Three.js product model",
    "sceneFamily": "spatial-dark",
    "motionRole": "gentle",
    "primaryState": "Dark 3D product ready for drag inspection",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/procedural-product-viewer.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/procedural-product-viewer.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport * as THREE from \"three\";\n\nexport type ProceduralProductViewerProps = {\n  productName?: string;\n  detailLabel?: string;\n  labels?: Partial<{\n    interactiveViewer: string;\n    staticPreview: string;\n    objectStudy: string;\n    dragToTurn: string;\n    staticBadge: string;\n    activateInteractive: string;\n    detailDescription: string;\n    resetView: string;\n  }>;\n  accent?: string;\n  activation?: \"intent\" | \"auto\";\n  className?: string;\n};\n\ntype RotationState = {\n  x: number;\n  y: number;\n  targetX: number;\n  targetY: number;\n  velocityX: number;\n  velocityY: number;\n  dragging: boolean;\n  pointerId: number | null;\n  lastX: number;\n  lastY: number;\n  lastTime: number;\n};\n\nconst LIMIT_X = 0.32;\nconst LIMIT_Y = 0.72;\nconst INITIALIZE_TIMEOUT_MS = 400;\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nconst yieldToMain = () => new Promise<void>((resolve) => window.setTimeout(resolve, 0));\n\nasync function precompileMaterialStages(\n  renderer: THREE.WebGLRenderer,\n  scene: THREE.Scene,\n  camera: THREE.Camera,\n  cancelled: () => boolean,\n) {\n  const renderables: Array<{ object: THREE.Object3D; visible: boolean; materialTypes: readonly string[] }> = [];\n  scene.traverse((object) => {\n    const material = \"material\" in object\n      ? (object as THREE.Mesh).material\n      : undefined;\n    if (!material) return;\n    const materials = Array.isArray(material) ? material : [material];\n    renderables.push({\n      object,\n      visible: object.visible,\n      materialTypes: materials.map((entry) => entry.type),\n    });\n  });\n  const materialTypes = [...new Set(renderables.flatMap((entry) => entry.materialTypes))];\n\n  for (const materialType of materialTypes) {\n    await yieldToMain();\n    if (cancelled()) return;\n    for (const entry of renderables) {\n      entry.object.visible = entry.visible && entry.materialTypes.includes(materialType);\n    }\n    renderer.compile(scene, camera);\n  }\n  for (const entry of renderables) entry.object.visible = entry.visible;\n}\n\nfunction makeProduct(\n  accent: THREE.ColorRepresentation,\n  resources: Set<{ dispose: () => void }>,\n) {\n  const group = new THREE.Group();\n  const track = <T extends { dispose: () => void }>(resource: T) => {\n    resources.add(resource);\n    return resource;\n  };\n\n  const shell = track(\n    new THREE.MeshBasicMaterial({\n      color: 0x262823,\n    }),\n  );\n  const dark = track(\n    new THREE.MeshBasicMaterial({\n      color: 0x0b0d0c,\n    }),\n  );\n  const accentMaterial = track(\n    new THREE.MeshBasicMaterial({\n      color: accent,\n    }),\n  );\n  const glass = track(\n    new THREE.MeshBasicMaterial({\n      color: 0xb6d7d5,\n      transparent: true,\n      opacity: 0.84,\n    }),\n  );\n\n  const bodyShape = new THREE.Shape();\n  const width = 1.18;\n  const height = 1.52;\n  const radius = 0.16;\n  const left = -width / 2;\n  const bottom = -height / 2;\n  bodyShape.moveTo(left + radius, bottom);\n  bodyShape.lineTo(left + width - radius, bottom);\n  bodyShape.quadraticCurveTo(left + width, bottom, left + width, bottom + radius);\n  bodyShape.lineTo(left + width, bottom + height - radius);\n  bodyShape.quadraticCurveTo(left + width, bottom + height, left + width - radius, bottom + height);\n  bodyShape.lineTo(left + radius, bottom + height);\n  bodyShape.quadraticCurveTo(left, bottom + height, left, bottom + height - radius);\n  bodyShape.lineTo(left, bottom + radius);\n  bodyShape.quadraticCurveTo(left, bottom, left + radius, bottom);\n  const bodyGeometry = track(\n    new THREE.ExtrudeGeometry(bodyShape, {\n      depth: 0.38,\n      bevelEnabled: true,\n      bevelSegments: 2,\n      bevelSize: 0.035,\n      bevelThickness: 0.035,\n      curveSegments: 6,\n      steps: 1,\n    }),\n  );\n  bodyGeometry.center();\n  const body = new THREE.Mesh(bodyGeometry, shell);\n  body.castShadow = true;\n  body.receiveShadow = true;\n  group.add(body);\n\n  const glassGeometry = track(new THREE.BoxGeometry(0.93, 0.76, 0.035));\n  const face = new THREE.Mesh(glassGeometry, glass);\n  face.position.set(0, 0.24, 0.249);\n  face.castShadow = true;\n  group.add(face);\n\n  const displayGeometry = track(new THREE.PlaneGeometry(0.72, 0.5));\n  const displayMaterial = track(\n    new THREE.MeshBasicMaterial({ color: 0x09110f, transparent: true, opacity: 0.96 }),\n  );\n  const display = new THREE.Mesh(displayGeometry, displayMaterial);\n  display.position.set(0, 0.25, 0.27);\n  group.add(display);\n\n  const barGeometry = track(new THREE.BoxGeometry(0.42, 0.035, 0.018));\n  const bar = new THREE.Mesh(barGeometry, accentMaterial);\n  bar.position.set(-0.1, 0.28, 0.286);\n  group.add(bar);\n\n  const shortBarGeometry = track(new THREE.BoxGeometry(0.2, 0.025, 0.018));\n  const shortBar = new THREE.Mesh(shortBarGeometry, shell);\n  shortBar.position.set(-0.21, 0.18, 0.286);\n  group.add(shortBar);\n\n  const dialGeometry = track(new THREE.CylinderGeometry(0.145, 0.145, 0.09, 24));\n  const dial = new THREE.Mesh(dialGeometry, dark);\n  dial.rotation.x = Math.PI / 2;\n  dial.position.set(0.3, -0.42, 0.27);\n  dial.castShadow = true;\n  group.add(dial);\n\n  const ringGeometry = track(new THREE.TorusGeometry(0.2, 0.025, 8, 24));\n  const ring = new THREE.Mesh(ringGeometry, accentMaterial);\n  ring.position.set(-0.27, -0.41, 0.282);\n  group.add(ring);\n\n  const footGeometry = track(new THREE.CylinderGeometry(0.44, 0.52, 0.09, 24));\n  const foot = new THREE.Mesh(footGeometry, dark);\n  foot.position.y = -0.81;\n  foot.castShadow = true;\n  group.add(foot);\n\n  const edgeGeometry = track(new THREE.EdgesGeometry(bodyGeometry, 35));\n  const edgeMaterial = track(\n    new THREE.LineBasicMaterial({ color: 0xf3d9af, transparent: true, opacity: 0.22 }),\n  );\n  group.add(new THREE.LineSegments(edgeGeometry, edgeMaterial));\n\n  group.rotation.x = -0.08;\n  return group;\n}\n\nexport function ProceduralProductViewer({\n  productName = \"Arc One\",\n  detailLabel = \"Tactile dial\",\n  labels,\n  accent = \"#dc774a\",\n  activation = \"intent\",\n  className = \"\",\n}: ProceduralProductViewerProps) {\n  const copy = {\n    interactiveViewer: \"Interactive 3D viewer. Drag or use arrow keys to rotate.\",\n    staticPreview: \"Static product preview.\",\n    objectStudy: \"Object study\",\n    dragToTurn: \"DRAG TO TURN\",\n    staticBadge: \"STATIC PREVIEW\",\n    activateInteractive: \"Explore 3D\",\n    detailDescription: \"Machined control with a quiet detent.\",\n    resetView: \"Reset view\",\n    ...labels,\n  };\n  const mountRef = useRef<HTMLDivElement>(null);\n  const detailId = useId();\n  const groupRef = useRef<THREE.Group | null>(null);\n  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);\n  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);\n  const sceneRef = useRef<THREE.Scene | null>(null);\n  const frameRef = useRef<number | null>(null);\n  const requestFrameRef = useRef<() => void>(() => undefined);\n  const focusAfterActivationRef = useRef(false);\n  const visibleRef = useRef(true);\n  const reduced = useReducedMotion() === true;\n  const reducedRef = useRef(reduced);\n  reducedRef.current = reduced;\n  const rotation = useRef<RotationState>({\n    x: -0.08,\n    y: 0,\n    targetX: -0.08,\n    targetY: 0,\n    velocityX: 0,\n    velocityY: 0,\n    dragging: false,\n    pointerId: null,\n    lastX: 0,\n    lastY: 0,\n    lastTime: 0,\n  });\n  const [detailOpen, setDetailOpen] = useState(false);\n  const [activationRequested, setActivationRequested] = useState(activation === \"auto\");\n  const [rendererReady, setRendererReady] = useState(false);\n  const [renderRequest, setRenderRequest] = useState<{ accent: string; version: number } | null>(null);\n  const renderVersionRef = useRef(0);\n\n  const draw = () => {\n    const renderer = rendererRef.current;\n    const scene = sceneRef.current;\n    const camera = cameraRef.current;\n    if (renderer && scene && camera) renderer.render(scene, camera);\n  };\n\n  const animate = () => {\n    frameRef.current = null;\n    if (!visibleRef.current) return;\n    const model = groupRef.current;\n    const state = rotation.current;\n    if (!model) return;\n\n    if (!reducedRef.current) {\n      if (!state.dragging) {\n        state.targetX += state.velocityX;\n        state.targetY += state.velocityY;\n        state.velocityX *= 0.84;\n        state.velocityY *= 0.84;\n        state.targetX += (-0.08 - state.targetX) * 0.025;\n        state.targetY += (0 - state.targetY) * 0.025;\n      }\n      state.targetX = clamp(state.targetX, -LIMIT_X, LIMIT_X);\n      state.targetY = clamp(state.targetY, -LIMIT_Y, LIMIT_Y);\n      state.x += (state.targetX - state.x) * 0.2;\n      state.y += (state.targetY - state.y) * 0.2;\n      model.rotation.set(state.x, state.y, 0);\n      draw();\n      const moving =\n        state.dragging ||\n        Math.abs(state.targetX - state.x) > 0.0005 ||\n        Math.abs(state.targetY - state.y) > 0.0005 ||\n        Math.abs(state.velocityX) > 0.0001 ||\n        Math.abs(state.velocityY) > 0.0001;\n      if (moving) frameRef.current = requestAnimationFrame(animate);\n    } else {\n      model.rotation.set(state.targetX, state.targetY, 0);\n      draw();\n    }\n  };\n\n  const requestFrame = () => {\n    if (frameRef.current === null && visibleRef.current) {\n      frameRef.current = requestAnimationFrame(animate);\n    }\n  };\n  requestFrameRef.current = requestFrame;\n\n  useEffect(() => {\n    if (activation === \"auto\") setActivationRequested(true);\n  }, [activation]);\n\n  useEffect(() => {\n    setRendererReady(false);\n    if (!activationRequested) return;\n    const initialize = () => {\n      renderVersionRef.current += 1;\n      setRenderRequest({ accent, version: renderVersionRef.current });\n    };\n    if (typeof window.requestIdleCallback === \"function\") {\n      const idleId = window.requestIdleCallback(initialize, { timeout: INITIALIZE_TIMEOUT_MS });\n      return () => window.cancelIdleCallback?.(idleId);\n    }\n    const timeoutId = window.setTimeout(initialize, 16);\n    return () => window.clearTimeout(timeoutId);\n  }, [accent, activationRequested]);\n\n  useEffect(() => {\n    const mount = mountRef.current;\n    if (!mount || !renderRequest) return;\n    let cancelled = false;\n    const resources = new Set<{ dispose: () => void }>();\n    const scene = new THREE.Scene();\n    const camera = new THREE.PerspectiveCamera(31, 1, 0.1, 100);\n    camera.position.set(0, 0.08, 4.35);\n\n    let renderer: THREE.WebGLRenderer;\n    try {\n      renderer = new THREE.WebGLRenderer({\n        alpha: true,\n        antialias: false,\n        powerPreference: \"high-performance\",\n      });\n    } catch {\n      setRendererReady(false);\n      if (activation === \"intent\") setActivationRequested(false);\n      return;\n    }\n    renderer.outputColorSpace = THREE.SRGBColorSpace;\n    renderer.shadowMap.enabled = false;\n    renderer.setClearColor(0x000000, 0);\n    renderer.domElement.setAttribute(\"aria-hidden\", \"true\");\n    renderer.domElement.style.width = \"100%\";\n    renderer.domElement.style.height = \"100%\";\n    renderer.domElement.style.display = \"block\";\n    mount.prepend(renderer.domElement);\n    const onContextLost = (event: Event) => {\n      event.preventDefault();\n      const state = rotation.current;\n      if (state.pointerId !== null && mount.hasPointerCapture(state.pointerId)) {\n        mount.releasePointerCapture(state.pointerId);\n      }\n      state.dragging = false;\n      state.pointerId = null;\n      state.velocityX = 0;\n      state.velocityY = 0;\n      if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);\n      frameRef.current = null;\n      setRendererReady(false);\n    };\n    const onContextRestored = () => {\n      setRendererReady(true);\n      requestFrameRef.current();\n    };\n    renderer.domElement.addEventListener(\"webglcontextlost\", onContextLost);\n    renderer.domElement.addEventListener(\"webglcontextrestored\", onContextRestored);\n\n    const product = makeProduct(renderRequest.accent, resources);\n    scene.add(product);\n\n    const groundGeometry = new THREE.CircleGeometry(1.12, 32);\n    const groundMaterial = new THREE.MeshBasicMaterial({\n      color: 0x292929,\n      transparent: true,\n      opacity: 0.08,\n      depthWrite: false,\n    });\n    resources.add(groundGeometry);\n    resources.add(groundMaterial);\n    const ground = new THREE.Mesh(groundGeometry, groundMaterial);\n    ground.rotation.x = -Math.PI / 2;\n    ground.position.set(0, -0.89, 0);\n    scene.add(ground);\n\n    rendererRef.current = renderer;\n    cameraRef.current = camera;\n    sceneRef.current = scene;\n    groupRef.current = product;\n\n    const resize = new ResizeObserver(([entry]) => {\n      const width = Math.max(1, entry.contentRect.width);\n      const height = Math.max(1, entry.contentRect.height);\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.35));\n      renderer.setSize(width, height, false);\n      camera.aspect = width / height;\n      camera.updateProjectionMatrix();\n      requestFrameRef.current();\n    });\n    resize.observe(mount);\n\n    let intersecting = true;\n    const intersection = new IntersectionObserver(([entry]) => {\n      intersecting = entry.isIntersecting;\n      visibleRef.current = intersecting && !document.hidden;\n      if (visibleRef.current) requestFrameRef.current();\n      else if (frameRef.current !== null) {\n        cancelAnimationFrame(frameRef.current);\n        frameRef.current = null;\n      }\n    });\n    const onVisibility = () => {\n      visibleRef.current = intersecting && !document.hidden;\n      if (visibleRef.current) requestFrameRef.current();\n      else if (frameRef.current !== null) {\n        cancelAnimationFrame(frameRef.current);\n        frameRef.current = null;\n      }\n    };\n    intersection.observe(mount);\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    void precompileMaterialStages(renderer, scene, camera, () => cancelled).then(() => {\n      if (cancelled) return;\n      setRendererReady(true);\n      requestFrameRef.current();\n    });\n\n    return () => {\n      cancelled = true;\n      renderer.domElement.removeEventListener(\"webglcontextlost\", onContextLost);\n      renderer.domElement.removeEventListener(\"webglcontextrestored\", onContextRestored);\n      resize.disconnect();\n      intersection.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);\n      resources.forEach((resource) => resource.dispose());\n      renderer.dispose();\n      renderer.forceContextLoss();\n      renderer.domElement.remove();\n      rendererRef.current = null;\n      cameraRef.current = null;\n      sceneRef.current = null;\n      groupRef.current = null;\n      frameRef.current = null;\n    };\n  }, [activation, renderRequest]);\n\n  useEffect(() => {\n    requestFrameRef.current();\n  }, [reduced]);\n\n  useEffect(() => {\n    if (!rendererReady || !focusAfterActivationRef.current) return;\n    focusAfterActivationRef.current = false;\n    mountRef.current?.focus({ preventScroll: true });\n  }, [rendererReady]);\n\n  const reset = () => {\n    const state = rotation.current;\n    state.targetX = -0.08;\n    state.targetY = 0;\n    state.velocityX = 0;\n    state.velocityY = 0;\n    requestFrame();\n  };\n\n  return (\n    <div\n      ref={mountRef}\n      data-webgl-root=\"procedural-product-viewer\"\n      role=\"group\"\n      tabIndex={rendererReady ? 0 : undefined}\n      aria-label={\n        rendererReady\n          ? `${productName}. ${copy.interactiveViewer}`\n          : `${productName}. ${copy.staticPreview}`\n      }\n      onPointerDown={(event) => {\n        if (!rendererReady || !(event.target instanceof HTMLCanvasElement)) return;\n        const state = rotation.current;\n        state.dragging = true;\n        state.pointerId = event.pointerId;\n        state.lastX = event.clientX;\n        state.lastY = event.clientY;\n        state.lastTime = performance.now();\n        event.currentTarget.setPointerCapture(event.pointerId);\n        event.currentTarget.focus({ preventScroll: true });\n      }}\n      onPointerMove={(event) => {\n        if (!rendererReady) return;\n        const state = rotation.current;\n        if (!state.dragging || state.pointerId !== event.pointerId) return;\n        const now = performance.now();\n        const elapsed = Math.max(8, now - state.lastTime);\n        const dx = event.clientX - state.lastX;\n        const dy = event.clientY - state.lastY;\n        state.targetY = clamp(state.targetY + dx * 0.008, -LIMIT_Y, LIMIT_Y);\n        state.targetX = clamp(state.targetX + dy * 0.006, -LIMIT_X, LIMIT_X);\n        state.velocityY = reduced ? 0 : (dx / elapsed) * 0.026;\n        state.velocityX = reduced ? 0 : (dy / elapsed) * 0.018;\n        state.lastX = event.clientX;\n        state.lastY = event.clientY;\n        state.lastTime = now;\n        requestFrame();\n      }}\n      onPointerUp={(event) => {\n        if (!rendererReady) return;\n        const state = rotation.current;\n        if (state.pointerId !== event.pointerId) return;\n        state.dragging = false;\n        state.pointerId = null;\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n        requestFrame();\n      }}\n      onPointerCancel={() => {\n        if (!rendererReady) return;\n        rotation.current.dragging = false;\n        rotation.current.pointerId = null;\n        rotation.current.velocityX = 0;\n        rotation.current.velocityY = 0;\n      }}\n      onLostPointerCapture={(event) => {\n        const state = rotation.current;\n        if (!state.dragging || state.pointerId !== event.pointerId) return;\n        state.dragging = false;\n        state.pointerId = null;\n        state.velocityX = 0;\n        state.velocityY = 0;\n      }}\n      onKeyDown={\n        rendererReady\n          ? (event) => {\n              const state = rotation.current;\n              const step = event.shiftKey ? 0.2 : 0.1;\n              if (event.key === \"ArrowLeft\") state.targetY -= step;\n              else if (event.key === \"ArrowRight\") state.targetY += step;\n              else if (event.key === \"ArrowUp\") state.targetX -= step;\n              else if (event.key === \"ArrowDown\") state.targetX += step;\n              else if (event.key === \"Home\") reset();\n              else return;\n              state.targetX = clamp(state.targetX, -LIMIT_X, LIMIT_X);\n              state.targetY = clamp(state.targetY, -LIMIT_Y, LIMIT_Y);\n              event.preventDefault();\n              requestFrame();\n            }\n          : undefined\n      }\n      className={`relative isolate min-h-[250px] w-full overflow-hidden rounded-[18px] border border-[#f4d5a8]/20 bg-[#11140f] outline-none focus-visible:ring-2 focus-visible:ring-[#8eb9ff] focus-visible:ring-offset-2 focus-visible:ring-offset-[#11140f] ${rendererReady ? \"cursor-grab touch-none active:cursor-grabbing\" : \"touch-pan-y\"} ${className}`}\n    >\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_72%_18%,rgba(221,119,74,.3),transparent_27%),radial-gradient(circle_at_25%_84%,rgba(94,155,145,.22),transparent_35%)]\" />\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 opacity-25 [background-image:linear-gradient(rgba(255,246,225,.09)_1px,transparent_1px),linear-gradient(90deg,rgba(255,246,225,.06)_1px,transparent_1px)] [background-size:24px_24px]\" />\n      <div\n        data-webgl-fallback=\"procedural-product-viewer\"\n        aria-hidden\n        className={`pointer-events-none absolute inset-0 grid place-items-center ${rendererReady ? \"opacity-0\" : \"opacity-100\"}`}\n      >\n        <div className=\"relative h-40 w-28 -rotate-3 rounded-[14px] border border-[#f5dfbb]/30 bg-[#252721] shadow-[0_26px_44px_-24px_rgba(0,0,0,.95)]\">\n          <span className=\"absolute inset-x-3 top-4 h-16 rounded-[10px] border border-white/10 bg-[#09110f] shadow-inner\">\n            <span className=\"absolute left-3 top-5 h-1 w-10 rounded-full\" style={{ background: accent }} />\n            <span className=\"absolute left-3 top-8 h-1 w-6 rounded-full bg-white/40\" />\n          </span>\n          <span className=\"absolute bottom-7 left-4 size-8 rounded-full border-[5px]\" style={{ borderColor: accent }} />\n          <span className=\"absolute bottom-7 right-4 size-7 rounded-full bg-[#080a09] shadow-[inset_2px_2px_4px_rgba(255,255,255,.16)]\" />\n          <span className=\"absolute -bottom-2 left-1/2 h-3 w-20 -translate-x-1/2 rounded-full bg-[#080a09] shadow-lg\" />\n        </div>\n      </div>\n\n      {!rendererReady && activation === \"intent\" ? (\n        <button\n          type=\"button\"\n          data-webgl-activation=\"procedural-product-viewer\"\n          disabled={activationRequested}\n          onClick={() => {\n            focusAfterActivationRef.current = true;\n            setActivationRequested(true);\n          }}\n          className=\"absolute left-1/2 top-1/2 z-30 min-h-11 -translate-x-1/2 -translate-y-1/2 rounded-full border border-[#f5dfbb]/25 bg-[#f7e7d0] px-4 text-[12px] font-semibold text-[#201c16] shadow-[0_12px_30px_-14px_rgba(0,0,0,.8)] outline-none focus-visible:ring-2 focus-visible:ring-[#8eb9ff] focus-visible:ring-offset-2 focus-visible:ring-offset-[#11140f] disabled:opacity-60\"\n        >\n          {copy.activateInteractive}\n        </button>\n      ) : null}\n\n      <div aria-hidden className=\"pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between p-4\">\n        <span>\n          <span className=\"block font-mono text-[9px] tracking-[.16em] text-[#f2d7ad]/60\">{copy.objectStudy}</span>\n          <strong className=\"mt-1 block text-[15px] font-medium tracking-[-0.03em] text-[#fff6e7]\">{productName}</strong>\n        </span>\n        <span className=\"rounded-full border border-white/15 bg-black/20 px-2.5 py-1 font-mono text-[9px] tracking-[.1em] text-[#f7e7d0]/75 backdrop-blur-sm\">\n          {rendererReady ? copy.dragToTurn : copy.staticBadge}\n        </span>\n      </div>\n\n      <button\n        type=\"button\"\n        aria-expanded={detailOpen}\n        aria-controls={detailId}\n        onClick={() => setDetailOpen((open) => !open)}\n        className=\"absolute left-[62%] top-[44%] z-20 grid size-11 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-[#8eb9ff] focus-visible:ring-offset-2 focus-visible:ring-offset-[#11140f]\"\n      >\n        <span className=\"grid size-5 place-items-center rounded-full border-2 border-[#fff2db] bg-[#dc774a] shadow-[0_0_0_5px_rgba(220,119,74,.18)]\">\n          <span className=\"size-1 rounded-full bg-[#fff3dd]\" />\n        </span>\n        <span className=\"sr-only\">{detailLabel}</span>\n      </button>\n\n      <div\n        id={detailId}\n        role=\"region\"\n        aria-label={detailLabel}\n        aria-hidden={!detailOpen}\n        className={`pointer-events-none absolute bottom-4 left-4 z-20 max-w-[170px] rounded-xl border border-white/15 bg-[#1b1d18]/92 px-3 py-2.5 backdrop-blur-sm ${reduced ? \"\" : \"transition-[opacity,transform] duration-200 [transition-timing-function:cubic-bezier(.2,.8,.2,1)]\"}`}\n        style={{\n          opacity: detailOpen ? 1 : 0,\n          transform: detailOpen ? \"translate3d(0,0,0)\" : \"translate3d(0,6px,0)\",\n        }}\n      >\n        <strong className=\"block text-[12px] font-medium text-[#fff4e2]\">{detailLabel}</strong>\n        <span className=\"mt-0.5 block text-[10px] leading-4 text-[#ead7ba]/65\">{copy.detailDescription}</span>\n      </div>\n\n      {rendererReady ? (\n        <button\n          type=\"button\"\n          onClick={reset}\n          className={`absolute bottom-3 right-3 z-20 grid size-11 place-items-center rounded-full border border-white/15 bg-[#f7e7d0] text-[#211c16] outline-none focus-visible:ring-2 focus-visible:ring-[#8eb9ff] focus-visible:ring-offset-2 focus-visible:ring-offset-[#11140f] ${reduced ? \"\" : \"transition-transform duration-150 active:scale-[0.96]\"}`}\n        >\n          <svg viewBox=\"0 0 20 20\" fill=\"none\" className=\"size-4\" aria-hidden>\n            <path d=\"M5.4 6.2A6 6 0 1 1 4.2 11M5.4 6.2V2.9M5.4 6.2H2.1\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          </svg>\n          <span className=\"sr-only\">{copy.resetView}</span>\n        </button>\n      ) : null}\n    </div>\n  );\n}\n"
    }
  ]
}
