{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"3d-configurator","type":"registry:block","title":"3D configurator","description":"\"product page runs 101\" — live material and finish switching with the price following. This one is fully working.","author":"Motion Menu","categories":["role-pricing","commerce","connected","three"],"dependencies":["gsap","three"],"registryDependencies":["@motionmenu/motion-menu-runtime"],"meta":{"number":"101","slug":"3d-configurator","framework":"react","docs":"https://motion-menu-two.vercel.app/p/3d-configurator.md"},"files":[{"path":"motion-menu/3d-configurator.html","type":"registry:file","target":"motion-menu/3d-configurator.html","content":"<article class=\"card\">\n        <header><span class=\"num\">101</span><h3>3D configurator</h3><div class=\"tags\"><i>Three.js</i><i>real</i></div><a class=\"format-action\" data-format-link data-pattern-slug=\"3d-configurator\" href=\"/r/3d-configurator.json?framework=react\" title=\"Open the React registry output\">React ↗</a></header>\n        <div class=\"demo\">\n          <canvas class=\"gl\" id=\"gl-config\"></canvas>\n          <div class=\"cfg-price\" id=\"cfg-price\">$249</div>\n          <div class=\"cfg-ui\" id=\"cfg-ui\"></div>\n        </div>\n        <p class=\"order\">Say: <b>\"product page runs 101\"</b> — live material and finish switching with the price following. This one is fully working.</p>\n      </article>"},{"path":"motion-menu/3d-configurator.css","type":"registry:file","target":"motion-menu/3d-configurator.css","content":".card {\n    border: 1px solid var(--line); border-radius: 14px; background: var(--card);\n    overflow: hidden; display: flex; flex-direction: column;\n  }\n\n.card > header { display: flex; align-items: baseline; gap: .8rem; padding: 1.1rem 1.2rem .9rem; border-bottom: 1px solid var(--line); }\n\n.card .num { font-family: var(--mono); color: var(--acc); font-size: 1.05rem; }\n\n.card h3 { font-size: 1.02rem; font-weight: 600; letter-spacing: -0.01em; flex: 1; }\n\n.tags { display: flex; gap: .35rem; }\n\n.tags i {\n    font: 400 .62rem/1 var(--mono); font-style: normal; color: var(--dim);\n    border: 1px solid var(--line); border-radius: 99px; padding: .28em .6em; white-space: nowrap;\n  }\n\n.demo {\n    min-height: 250px; position: relative; display: flex; align-items: center; justify-content: center;\n    background: var(--bg2); overflow: hidden; flex: 1;\n  }\n\n.order { padding: .85rem 1.2rem; font-family: var(--mono); font-size: .72rem; color: var(--dim); border-top: 1px solid var(--line); }\n\n.order b { color: var(--txt); font-weight: 500; }\n\n.demo .btn.corner { position: absolute; top: 12px; right: 12px; padding: .55em 1em; font-size: .68rem; z-index: 5; }\n\ncanvas.gl { width: 100%; height: 100%; display: block; position: absolute; inset: 0; }\n\n.ai-card .num { color: var(--acc2) !important; }\n\n.tabs button.on { color: #0a0a0e; }\n\n.car-dots i.on { background: var(--acc); transform: scale(1.3); }\n\n.chap-dots i.on { background: var(--acc); }\n\n/* ---------- Section N · commerce & connected ---------- */\n  .cfg-ui { position: absolute; bottom: 14px; left: 0; right: 0; display: flex; gap: .5rem; justify-content: center; align-items: center; z-index: 5; flex-wrap: wrap; }\n\n.cfg-swatch { width: 26px; height: 26px; border-radius: 50%; border: 2px solid transparent; padding: 0; }\n\n.cfg-swatch.on { border-color: #fff; }\n\n.cfg-finish { font: 500 .62rem var(--mono); color: var(--txt); background: transparent; border: 1px solid rgba(255,255,255,.3); border-radius: 99px; padding: .4em .9em; }\n\n.cfg-finish.on { border-color: var(--acc); color: var(--acc); }\n\n.cfg-price { position: absolute; top: 12px; right: 14px; font: 700 1.1rem var(--sans); color: var(--txt); z-index: 5; font-variant-numeric: tabular-nums; }\n\n.vfx-btns button.on { border-color: var(--acc); color: var(--acc); }\n\n.sw-chip.on { border-color: #fff; transform: scale(1.08); }\n\n.calc .c-tiers button.on { border-color: var(--acc); color: var(--acc); }\n\n.dims .d-btns button.on { border-color: var(--acc); color: var(--acc); }\n\n.samplereq .sr-step.on { display: block; }\n\n.samplereq .sr-picks button.on { border-color: #fff; }\n\n.volmx tr.on td { background: rgba(183,255,46,.08); color: var(--txt); }\n\n.intwall .iw-filters button.on { border-color: var(--acc); color: var(--acc); }\n\n.lc-stages button.on { border-color: var(--acc); color: var(--acc); }\n\n.install-flow .if-step.on { border-color: var(--acc); color: var(--txt); }\n\n.demo:has(.hascheck input:checked) .hasbtn { border-color: var(--acc); color: var(--acc); pointer-events: auto; opacity: 1; }\n\n.bs-days button.on, .bs-times button.on { border-color: var(--acc); color: var(--acc); }\n\n.gw-box.on { border-color: #fff; }\n\n.stone.on { color: var(--acc); }\n\n.hot-chip.on { opacity: 1; }\n\n.lot-chip.on { color: var(--acc); border-color: var(--acc); }\n\n.gx-veil.on { opacity: 1; }\n\n.fp-pal.on { border-color: var(--acc); color: var(--acc); }\n\n.tier-layer.on { opacity: 1; }\n\n.tier-btn.on { border-color: var(--acc); color: var(--acc); }"},{"path":"components/motion-menu/3d-configurator.tsx","type":"registry:component","content":"// @ts-nocheck — generated wrapper; inlined pattern JS is imperative DOM code, not authored TS.\n\"use client\";\n// 3D configurator — Motion Menu\n// Calls the Motion Menu shared runtime (webglDemo / canvas2dDemo / art helpers).\n// Install `motion-menu-runtime` and mount it once at the app root, or the effect\n// below throws ReferenceError on first paint.\nimport { useEffect, useRef } from \"react\";\n\nconst MARKUP = \"<article class=\\\"card\\\">\\n        <header><span class=\\\"num\\\">101</span><h3>3D configurator</h3><div class=\\\"tags\\\"><i>Three.js</i><i>real</i></div><a class=\\\"format-action\\\" data-format-link data-pattern-slug=\\\"3d-configurator\\\" href=\\\"/r/3d-configurator.json?framework=react\\\" title=\\\"Open the React registry output\\\">React \\u2197</a></header>\\n        <div class=\\\"demo\\\">\\n          <canvas class=\\\"gl\\\" id=\\\"gl-config\\\"></canvas>\\n          <div class=\\\"cfg-price\\\" id=\\\"cfg-price\\\">$249</div>\\n          <div class=\\\"cfg-ui\\\" id=\\\"cfg-ui\\\"></div>\\n        </div>\\n        <p class=\\\"order\\\">Say: <b>\\\"product page runs 101\\\"</b> \\u2014 live material and finish switching with the price following. This one is fully working.</p>\\n      </article>\";\n\nexport function 3dConfigurator() {\n  const hostRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const host = hostRef.current;\n    if (!host) return;\n    const $ = (s: string) => host.querySelector(s) as HTMLElement | null;\n    let disposed = false;\n    const cleanup: Array<() => void> = [];\n\n    (async () => {\n      if (disposed) return;\n      try {\n        /* ---- 101 · 3D configurator ---- */\n        {\n          const COLORS = [\n            ['silver', 0xd8d8e0, 0], ['gold', 0xd4af6a, 40], ['violet', 0x8b6cff, 25],\n          ];\n          const FINISHES = [['polished', 0.10, 1.0, 60], ['matte', 0.55, 0.65, 0]];\n          const state = { color: 0, finish: 0 };\n          let matRef = null;\n          webglDemo('gl-config', ({ scene, camera }) => {\n            camera.position.z = 5.8;\n            const mat = new THREE.MeshPhysicalMaterial({\n              color: COLORS[state.color][1], metalness: FINISHES[state.finish][2],\n              roughness: FINISHES[state.finish][1], envMapIntensity: 1.3,\n            });\n            matRef = mat;\n            const knot = new THREE.Mesh(new THREE.TorusKnotGeometry(1, 0.34, 200, 32), mat);\n            scene.add(knot);\n            const key = new THREE.DirectionalLight(0xffffff, 1.1);\n            key.position.set(4, 6, 4);\n            scene.add(key);\n            return { update(t, delta) { knot.rotation.y += delta * 0.4; knot.rotation.x = Math.sin(t * 0.3) * 0.2; } };\n          });\n          const ui = document.getElementById('cfg-ui');\n          const priceEl = document.getElementById('cfg-price');\n          const priceObj = { v: 249 };\n          function apply() {\n            if (matRef) {\n              matRef.color.setHex(COLORS[state.color][1]);\n              matRef.roughness = FINISHES[state.finish][1];\n              matRef.metalness = FINISHES[state.finish][2];\n            }\n            const price = 199 + COLORS[state.color][2] + FINISHES[state.finish][3];\n            gsap.to(priceObj, { v: price, duration: 0.5, onUpdate: () => (priceEl.textContent = '$' + Math.round(priceObj.v)) });\n            ui.querySelectorAll('.cfg-swatch').forEach((b, i) => b.classList.toggle('on', i === state.color));\n            ui.querySelectorAll('.cfg-finish').forEach((b, i) => b.classList.toggle('on', i === state.finish));\n          }\n          COLORS.forEach(([name, hex], i) => {\n            const b = document.createElement('button');\n            b.className = 'cfg-swatch' + (i === 0 ? ' on' : '');\n            b.style.background = '#' + hex.toString(16).padStart(6, '0');\n            b.title = name;\n            b.addEventListener('click', () => { state.color = i; apply(); });\n            ui.appendChild(b);\n          });\n          FINISHES.forEach(([name], i) => {\n            const b = document.createElement('button');\n            b.className = 'cfg-finish' + (i === 0 ? ' on' : '');\n            b.textContent = name;\n            b.addEventListener('click', () => { state.finish = i; apply(); });\n            ui.appendChild(b);\n          });\n          apply();\n        }\n      } catch (err: any) {\n        console.warn(\"[motion-menu] 3d-configurator failed to start:\", err);\n      }\n    })();\n\n    return () => {\n      disposed = true;\n      cleanup.forEach((fn) => { try { fn(); } catch (_) {} });\n    };\n  }, []);\n\n  return <div ref={hostRef} dangerouslySetInnerHTML={{ __html: MARKUP }} />;\n}\n\nexport default 3dConfigurator;\n"}]}