{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chrome-logo",
  "type": "registry:block",
  "title": "Chrome Logo",
  "description": "Any inline SVG extruded into bevelled metal (chrome, gold, gunmetal or iridescent oil slick) that reflects white softboxes, a hard horizon and coloured strip lights, over a floor that mirrors it. Drag it to spin: it keeps the momentum on a flywheel, then settles back to a slow idle.",
  "docs": "Chrome Logo was added to public/vividsites/chrome-logo.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/chrome-logo",
  "categories": [
    "3d",
    "3d-objects"
  ],
  "files": [
    {
      "path": "components/vividsites/chrome-logo.html",
      "type": "registry:file",
      "target": "public/vividsites/chrome-logo.html",
      "content": "<!--\n  vividsites · chrome-logo\n  Any inline SVG, extruded into bevelled, polished metal. The mark is lit by a\n  studio of white softboxes, a hard horizon and coloured strip lights (the metal\n  picks up their colour), stands over a graphite floor that reflects it, and turns\n  on a flywheel: drag it (or use the arrow keys) and it keeps the spin, then\n  bleeds back to a slow idle.\n\n  Your logo: replace the <svg class=\"vs-chrome-logo-mark\"> with your own. Every\n  filled <path>, <circle>, <rect>, <polygon> is extruded. Keep shapes from\n  overlapping (overlaps z-fight on the faces). The SVG is also the no-WebGL fallback.\n\n  Customise (attributes, read at mount; all live through the JS API too):\n    data-finish   chrome | gold | gunmetal | oil (thin-film iridescent)   (default chrome)\n    data-depth    extrusion depth, relative to the mark's size            (default .075)\n    data-bevel    bevel size, relative                                    (default .034)\n  CSS variables:\n    --vs-chrome-logo-light   the strip lights' colour, seen in the metal  (#5cc8ff)\n    --vs-chrome-logo-bg      the stage                                    (#111214)\n    --vs-chrome-logo-idle    idle spin, radians per second, 0 = still     (.4)\n    --vs-chrome-logo-height  component height                             (100svh)\n  JS:\n    el.vsChromeLogo.set({ finish: 'gold', light: '#ff7a59', spin: .8 })\n    el.vsChromeLogo.spin(8)      // a flick, radians per second\n    el.vsChromeLogo.destroy()\n  Needs: three@0.170.0 + SVGLoader (importmap below).\n-->\n<div class=\"vs-chrome-logo\" data-finish=\"chrome\" data-depth=\"0.075\" data-bevel=\"0.034\" tabindex=\"0\" role=\"img\" aria-label=\"A chrome logo. Drag or use the arrow keys to spin it.\">\n  <svg class=\"vs-chrome-logo-mark\" viewBox=\"0 0 240 240\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n    <!-- \"Link\": two woven rings (over at the top, under at the bottom). Replace with your own mark. -->\n    <path d=\"M150.39 43.38A77 77 0 1 1 84.93 95.72A12 12 0 0 1 103.19 89.67A37 37 0 0 1 114.87 103.02A12 12 0 0 1 115.95 111.03A43 43 0 1 0 159.15 77.02A12 12 0 0 1 149.69 71.96A83 83 0 0 0 142.79 63.49A12 12 0 0 1 150.39 43.38Z\"/>\n    <path d=\"M89.61 196.62A77 77 0 1 1 155.07 144.28A12 12 0 0 1 136.81 150.33A37 37 0 0 1 125.13 136.98A12 12 0 0 1 124.05 128.97A43 43 0 1 0 80.85 162.98A12 12 0 0 1 90.31 168.04A83 83 0 0 0 97.21 176.51A12 12 0 0 1 89.61 196.62Z\"/>\n  </svg>\n  <canvas class=\"vs-chrome-logo-canvas\"></canvas>\n</div>\n\n<style>\n  .vs-chrome-logo {\n    --vs-chrome-logo-light: #5cc8ff;\n    --vs-chrome-logo-bg: #111214;\n    --vs-chrome-logo-idle: .4;\n    --vs-chrome-logo-height: 100svh;\n    position: relative; width: 100%; height: var(--vs-chrome-logo-height); min-height: 420px; overflow: hidden; isolation: isolate;\n    background:\n      radial-gradient(50% 42% at 50% 44%, color-mix(in srgb, var(--vs-chrome-logo-light) 7%, transparent), transparent 72%),\n      linear-gradient(to bottom, var(--vs-chrome-logo-bg) 62%, color-mix(in srgb, var(--vs-chrome-logo-bg) 80%, #000) 100%);\n    cursor: grab; touch-action: pan-y; outline: none; -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none;\n    transition: background .8s cubic-bezier(.16,1,.3,1);\n  }\n  .vs-chrome-logo.is-dragging { cursor: grabbing; }\n  .vs-chrome-logo:focus-visible { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--vs-chrome-logo-light) 70%, transparent); }\n  .vs-chrome-logo-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0; transform: scale(1.03); transition: opacity 1.2s cubic-bezier(.16,1,.3,1), transform 2.4s cubic-bezier(.16,1,.3,1); }\n  .vs-chrome-logo[data-vs-state=\"ready\"] .vs-chrome-logo-canvas { opacity: 1; transform: none; }\n  /* the source SVG: hidden once the metal is up; a brushed-metal fallback without WebGL */\n  .vs-chrome-logo-mark { position: absolute; left: 50%; top: 46%; width: min(56%, 56vh); height: auto; transform: translate(-50%, -50%); visibility: hidden; }\n  .vs-chrome-logo[data-vs-state=\"nogl\"] .vs-chrome-logo-mark { visibility: visible; fill: #c9ced6; filter: drop-shadow(0 18px 30px rgba(0,0,0,.6)); }\n  .vs-chrome-logo::after { /* vignette + a whisper of grain */\n    content: \"\"; position: absolute; inset: 0; pointer-events: none;\n    background: radial-gradient(115% 90% at 50% 45%, transparent 55%, rgba(0,0,0,.55)),\n      url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 0 0 0 .09 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>\");\n  }\n</style>\n\n<script type=\"importmap\">{ \"imports\": { \"three\": \"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js\", \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/\" } }</script>\n<script type=\"module\">\nimport * as THREE from 'three';\nimport { SVGLoader } from 'three/addons/loaders/SVGLoader.js';\nimport { toCreasedNormals } from 'three/addons/utils/BufferGeometryUtils.js';\n\nconst css = (el, n, f) => getComputedStyle(el).getPropertyValue(n).trim() || f;\nconst FINISHES = {\n  chrome:   { color: '#eef1f5', rough: 0.07, irid: 0, env: 1.8, coat: 1 },\n  gold:     { color: '#f2c26e', rough: 0.1,  irid: 0, env: 1.7, coat: 1 },\n  gunmetal: { color: '#7b8088', rough: 0.2,  irid: 0, env: 1.6, coat: 0.7 },\n  oil:      { color: '#c9ced6', rough: 0.07, irid: 1, env: 1.8, coat: 1 },\n};\n\n// the room the metal reflects: a dark dome with a hot horizon, white softboxes, and strip lights in the light colour\nfunction buildStudio(light) {\n  const env = new THREE.Scene();\n  const U = { uLight: { value: light } };\n  env.add(new THREE.Mesh(new THREE.SphereGeometry(20, 64, 32), new THREE.ShaderMaterial({\n    side: THREE.BackSide, uniforms: U,\n    vertexShader: 'varying vec3 vD; void main(){ vD = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',\n    fragmentShader: `uniform vec3 uLight; varying vec3 vD;\n      void main(){\n        float y = vD.y;\n        vec3 sky = mix(vec3(0.26, 0.27, 0.29), vec3(0.018, 0.02, 0.024), smoothstep(0.0, 0.3, y));\n        vec3 ground = mix(vec3(0.01), vec3(0.03, 0.03, 0.032), smoothstep(-0.05, -0.8, y));\n        vec3 c = y > 0.0 ? sky : ground;\n        c += vec3(2.4, 2.4, 2.35) * exp(-abs(y) * 45.0);               // the hard horizon chrome needs\n        c += uLight * 1.3 * exp(-abs(y + 0.08) * 16.0);                  // a coloured glow just under it\n        gl_FragColor = vec4(c, 1.0);\n      }` })));\n  const mats = [];\n  const box = (w, h, pos, color, k, tinted) => {\n    const m = new THREE.Mesh(new THREE.PlaneGeometry(w, h), new THREE.MeshBasicMaterial({ color: new THREE.Color(color).multiplyScalar(k), side: THREE.DoubleSide }));\n    m.position.copy(pos); m.lookAt(0, 0, 0); env.add(m); if (tinted) mats.push([m.material, k]);\n  };\n  box(0.8, 14, new THREE.Vector3(-8, 2, 6), '#ffffff', 7);      // key strip, white\n  box(10, 2, new THREE.Vector3(0, 9, 3), '#ffffff', 2.4);       // top\n  box(1.1, 14, new THREE.Vector3(9, 1, 4), '#ffffff', 2.2);     // fill strip, white\n  box(2.2, 14, new THREE.Vector3(8, 0, -6), light, 8, true);    // coloured strips: behind right, behind left, and a low one in front\n  box(2.2, 14, new THREE.Vector3(-8.5, 0, -5), light, 6, true);\n  box(12, 1.2, new THREE.Vector3(0, -3.5, 8), light, 3.5, true);\n  return { env, recolor: (c) => { U.uLight.value.copy(c); mats.forEach(([m, k]) => m.color.copy(c).multiplyScalar(k)); } };\n}\n\nfunction buildGeometry(svgEl, depthK, bevelK) {\n  const data = new SVGLoader().parse(new XMLSerializer().serializeToString(svgEl));\n  const shapes = [];\n  data.paths.forEach((p) => { const st = p.userData && p.userData.style; if (st && st.fill === 'none') return; SVGLoader.createShapes(p).forEach((s) => shapes.push(s)); });\n  const probe = new THREE.ShapeGeometry(shapes); probe.computeBoundingBox();\n  const bb = probe.boundingBox; const size = Math.max(bb.max.x - bb.min.x, bb.max.y - bb.min.y); probe.dispose();\n  // flatten every outline and drop near-duplicate points: where two arcs meet (or a path closes) float error\n  // leaves ~0-length edges, and the bevel turns each one into a spike\n  const eps = size * 2e-4;\n  const clean = (pts) => { const o = []; for (const p of pts) if (!o.length || o[o.length - 1].distanceTo(p) > eps) o.push(p); while (o.length > 3 && o[0].distanceTo(o[o.length - 1]) <= eps) o.pop(); return o; };\n  const flat = shapes.map((sh) => { const x = sh.extractPoints(40); const n = new THREE.Shape(clean(x.shape)); n.holes = x.holes.map((h) => new THREE.Path(clean(h))); return n; });\n  const geo = new THREE.ExtrudeGeometry(flat, { depth: size * depthK, bevelEnabled: true, bevelThickness: size * bevelK, bevelSize: size * bevelK * 0.9,\n    bevelOffset: -size * bevelK * 0.9, bevelSegments: 12, curveSegments: 1 }); // the bevel eats inward, so the silhouette = your SVG\n  geo.center(); geo.scale(2.4 / size, -2.4 / size, -2.4 / size); // SVG is y-down: flip y, and z too so the winding stays front-facing; longer side = 2.4 units\n  geo.deleteAttribute('uv');\n  // ExtrudeGeometry ships flat per-face normals, which facet every curve into visible bands in a mirror\n  // finish. Re-smooth the walls + bevel on their own (under 50 deg, so real corners stay crisp) and give\n  // the caps exact +-z normals, so their long fan triangles don't streak.\n  const pos = geo.attributes.position, nrm = geo.attributes.normal;\n  const isCap = new Uint8Array(pos.count); // one caps group per shape (materialIndex 0), then its walls (1)\n  geo.groups.forEach((g) => { if (g.materialIndex === 0) isCap.fill(1, g.start, g.start + g.count); });\n  const side = new THREE.BufferGeometry(), idx = [];\n  for (let i = 0; i < pos.count; i++) if (!isCap[i]) idx.push(i);\n  side.setAttribute('position', new THREE.Float32BufferAttribute(idx.flatMap((i) => [pos.getX(i), pos.getY(i), pos.getZ(i)]), 3));\n  const sn = toCreasedNormals(side, THREE.MathUtils.degToRad(50)).attributes.normal;\n  idx.forEach((i, k) => nrm.setXYZ(i, sn.getX(k), sn.getY(k), sn.getZ(k)));\n  for (let i = 0; i < pos.count; i++) if (isCap[i]) nrm.setXYZ(i, 0, 0, Math.sign(nrm.getZ(i)) || 1);\n  nrm.needsUpdate = true; side.dispose();\n  geo.computeBoundingBox();\n  return geo;\n}\n\nfunction mount(el) {\n  const canvas = el.querySelector('.vs-chrome-logo-canvas');\n  const svg = el.querySelector('.vs-chrome-logo-mark');\n  const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\n  let renderer;\n  try { renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'high-performance' }); } catch (e) { el.dataset.vsState = 'nogl'; return null; }\n  if (!renderer.getContext()) { el.dataset.vsState = 'nogl'; return null; }\n  renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.05;\n  renderer.setClearColor(0x000000, 0);\n\n  const light = new THREE.Color(css(el, '--vs-chrome-logo-light', '#5cc8ff')), lightTo = light.clone();\n  let idle = reduced ? 0 : parseFloat(css(el, '--vs-chrome-logo-idle', '.4'));\n  const scene = new THREE.Scene();\n  const studio = buildStudio(light.clone());\n  const pmrem = new THREE.PMREMGenerator(renderer);\n  let envRT = null;\n  const bake = () => { studio.recolor(light); const rt = pmrem.fromScene(studio.env, 0.008); if (envRT) envRT.dispose(); envRT = rt; scene.environment = rt.texture; };\n  bake();\n  const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);\n\n  // flat caps get a gentle pillow in the shader, so each face sweeps sky → horizon → floor like real chrome type\n  const geo = buildGeometry(svg, parseFloat(el.dataset.depth || 0.075), parseFloat(el.dataset.bevel || 0.034));\n  const MB = geo.boundingBox, MW = MB.max.x - MB.min.x, MH = MB.max.y - MB.min.y;\n  const PILLOW = 0.2, FLOOR = -MH / 2 - 0.12; // the mark hovers a little above a glossy floor\n  const patch = (sh, fade) => {\n    sh.uniforms.uFloor = { value: FLOOR };\n    sh.vertexShader = sh.vertexShader\n      .replace('#include <common>', '#include <common>\\nvarying vec3 vOP; varying vec3 vON; varying vec3 vBX; varying vec3 vBY; varying vec3 vBZ; varying float vRy;')\n      .replace('#include <begin_vertex>', '#include <begin_vertex>\\nvOP = position; vON = normal; vBX = normalize(normalMatrix * vec3(1.0, 0.0, 0.0)); vBY = normalize(normalMatrix * vec3(0.0, 1.0, 0.0)); vBZ = normalize(normalMatrix * vec3(0.0, 0.0, 1.0)); vRy = (modelMatrix * vec4(transformed, 1.0)).y;');\n    sh.fragmentShader = sh.fragmentShader\n      .replace('#include <common>', '#include <common>\\nvarying vec3 vOP; varying vec3 vON; varying vec3 vBX; varying vec3 vBY; varying vec3 vBZ; varying float vRy; uniform float uFloor;')\n      .replace('#include <normal_fragment_maps>', `#include <normal_fragment_maps>\n        { // a gentle pillow over the faces (and, tapering, the bevel), so flat caps sweep sky > horizon > floor\n          vec3 on = normalize(vON + vec3(vOP.xy * ${PILLOW.toFixed(3)} * abs(vON.z), 0.0));\n          normal = faceDirection * normalize(vBX * on.x + vBY * on.y + vBZ * on.z);\n        }`)\n      .replace('#include <clearcoat_normal_fragment_begin>', '#include <clearcoat_normal_fragment_begin>\\n clearcoatNormal = normal;');\n    if (fade) sh.fragmentShader = sh.fragmentShader.replace('#include <dithering_fragment>', '#include <dithering_fragment>\\nfloat fd = clamp((uFloor - vRy) / 1.3, 0.0, 1.0); gl_FragColor.a *= 0.26 * (1.0 - fd) * (1.0 - fd);');\n  };\n  const F0 = FINISHES[el.dataset.finish] || FINISHES.chrome;\n  const metal = new THREE.MeshPhysicalMaterial({ color: new THREE.Color(F0.color), metalness: 1, roughness: F0.rough, envMapIntensity: F0.env, clearcoat: F0.coat, clearcoatRoughness: 0.03,\n    iridescence: F0.irid, iridescenceIOR: 1.8, iridescenceThicknessRange: [120, 420] });\n  metal.onBeforeCompile = (sh) => patch(sh, false);\n  const pivot = new THREE.Group(); scene.add(pivot);\n  const mesh = new THREE.Mesh(geo, metal); pivot.add(mesh);\n\n  // the glossy floor: the same mark mirrored under it, fading with distance from the floor line\n  const reflMat = metal.clone(); reflMat.transparent = true; reflMat.depthWrite = false;\n  reflMat.onBeforeCompile = (sh) => patch(sh, true);\n  reflMat.customProgramCacheKey = () => 'vs-chrome-refl';\n  const refl = new THREE.Mesh(geo, reflMat); refl.matrixAutoUpdate = false; scene.add(refl);\n  const mirror = new THREE.Matrix4().makeTranslation(0, 2 * FLOOR, 0).multiply(new THREE.Matrix4().makeScale(1, -1, 1));\n\n  const S = { yaw: -0.5, w: idle, pitch: -0.08, pv: 0, pitchT: -0.08, drag: false, lx: 0, ly: 0, lt: 0, visible: true, raf: 0, last: 0, t: 0, hoverX: 0, hoverY: 0, intro: 0 };\n  const fin = { rough: F0.rough, irid: F0.irid, env: F0.env, coat: F0.coat, color: new THREE.Color(F0.color) }, finTo = { ...fin, color: fin.color.clone() };\n\n  function resize() {\n    const w = el.clientWidth, h = el.clientHeight; if (!w || !h) return;\n    renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(w, h, false);\n    camera.aspect = w / h;\n    // the mark fills ~58% of the height, or ~88% of the width on a narrow screen; its reflection sits below\n    const vh = Math.max(MH / 0.58, MW / 0.88 / camera.aspect);\n    const dist = vh / 2 / Math.tan(THREE.MathUtils.degToRad(camera.fov / 2));\n    const cy = -MH * 0.14;\n    camera.position.set(0, cy + dist * 0.07, dist); camera.lookAt(0, cy, 0); camera.updateProjectionMatrix();\n  }\n\n  function set(o = {}) {\n    if (o.finish && FINISHES[o.finish]) { const f = FINISHES[o.finish]; el.dataset.finish = o.finish; Object.assign(finTo, { rough: f.rough, irid: f.irid, env: f.env, coat: f.coat }); finTo.color = new THREE.Color(f.color); S.w += reduced ? 0 : 2.2 * Math.sign(S.w || 1); }\n    if (o.light) { lightTo.set(o.light); el.style.setProperty('--vs-chrome-logo-light', o.light); }\n    if (o.spin != null) { idle = reduced ? 0 : +o.spin; }\n    if (reduced) { Object.assign(fin, finTo, { color: finTo.color.clone() }); light.copy(lightTo); bake(); }\n    kick();\n  }\n\n  function frame(now) {\n    S.raf = 0;\n    const dt = Math.min(0.05, (now - (S.last || now)) / 1000); S.last = now; S.t += dt;\n    if (!S.drag) {\n      S.w += (idle - S.w) * (1 - Math.exp(-dt * 0.9));        // the flywheel bleeds back to idle\n      S.yaw += S.w * dt;\n      if (idle === 0) { const target = Math.round(S.yaw / Math.PI) * Math.PI; S.w += (target - S.yaw) * dt * 6; S.w *= Math.exp(-dt * 4); } // settle face-on when still\n      const k = 140, c = 20; S.pv += (-k * (S.pitch - S.pitchT) - c * S.pv) * dt; S.pitch += S.pv * dt;\n    }\n    S.intro = Math.min(1, S.intro + dt / 2.2);\n    const e = 1 - Math.pow(1 - S.intro, 4);\n    pivot.rotation.set(S.pitch + (reduced ? 0 : Math.sin(S.t * 0.7) * 0.03) + S.hoverY * 0.1, S.yaw, S.hoverX * -0.05);\n    pivot.position.y = 0.06 + (reduced ? 0 : Math.sin(S.t * 1.1) * 0.045) + (1 - e) * -0.4;\n    pivot.scale.setScalar(0.9 + 0.1 * e);\n    // finish + light colour ease\n    const kk = 1 - Math.exp(-dt * 5);\n    fin.rough += (finTo.rough - fin.rough) * kk; fin.irid += (finTo.irid - fin.irid) * kk; fin.env += (finTo.env - fin.env) * kk; fin.coat += (finTo.coat - fin.coat) * kk; fin.color.lerp(finTo.color, kk);\n    for (const m of [metal, reflMat]) { m.roughness = fin.rough; m.iridescence = fin.irid; m.envMapIntensity = fin.env; m.clearcoat = fin.coat; m.color.copy(fin.color); }\n    const dl = Math.abs(light.r - lightTo.r) + Math.abs(light.g - lightTo.g) + Math.abs(light.b - lightTo.b);\n    if (dl > 0.004) { light.lerp(lightTo, dl > 0.05 ? 0.18 : 1); bake(); } // re-bake the studio while the strips change colour\n    pivot.updateMatrixWorld(); mesh.updateMatrixWorld();\n    refl.matrix.copy(mirror).multiply(mesh.matrixWorld); refl.matrixWorldNeedsUpdate = true;\n    scene.environmentRotation.y = reduced ? 0.5 : 0.5 + Math.sin(S.t * 0.15) * 0.45; // the strips drift, so highlights run across the metal\n    renderer.render(scene, camera);\n    if (el.dataset.vsState !== 'ready') el.dataset.vsState = 'ready';\n    const busy = S.drag || Math.abs(S.w) > 1e-3 || Math.abs(S.pv) > 1e-3 || S.intro < 1 || !reduced || dl > 0.004 || Math.abs(fin.rough - finTo.rough) + Math.abs(fin.irid - finTo.irid) > 1e-3;\n    if (S.visible && !document.hidden && busy) S.raf = requestAnimationFrame(frame);\n  }\n  const kick = () => { if (!S.raf && S.visible) { S.last = performance.now(); S.raf = requestAnimationFrame(frame); } };\n\n  // ---- drag with a flywheel\n  const onDown = (e) => { if (e.button > 0) return; S.drag = true; S.lx = e.clientX; S.ly = e.clientY; S.lt = performance.now(); S.w = 0; el.classList.add('is-dragging'); el.setPointerCapture(e.pointerId); kick(); };\n  const onMove = (e) => {\n    const r = el.getBoundingClientRect();\n    if (e.pointerType === 'mouse') { S.hoverX = ((e.clientX - r.left) / r.width - 0.5) * 2; S.hoverY = ((e.clientY - r.top) / r.height - 0.5) * 2; }\n    if (!S.drag) { kick(); return; }\n    const now = performance.now(), dt = Math.max(1, now - S.lt) / 1000;\n    const dx = (e.clientX - S.lx) / Math.min(r.width, 900) * Math.PI * 1.6, dy = (e.clientY - S.ly) / r.height * 1.4;\n    S.yaw += dx; S.w = S.w * 0.4 + (dx / dt) * 0.6;\n    S.pitch = THREE.MathUtils.clamp(S.pitch + dy, -0.6, 0.6);\n    S.lx = e.clientX; S.ly = e.clientY; S.lt = now; kick();\n  };\n  const onUp = () => { if (!S.drag) return; S.drag = false; el.classList.remove('is-dragging'); if (performance.now() - S.lt > 80) S.w *= 0.2; S.w = THREE.MathUtils.clamp(S.w, -14, 14); kick(); };\n  const onLeave = () => { S.hoverX = 0; S.hoverY = 0; kick(); };\n  const onKey = (e) => { if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); S.w += (e.key === 'ArrowLeft' ? -1 : 1) * 3; kick(); } if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); S.pv += (e.key === 'ArrowUp' ? -1 : 1) * 3; kick(); } };\n  el.addEventListener('pointerdown', onDown); el.addEventListener('pointermove', onMove); el.addEventListener('pointerup', onUp); el.addEventListener('pointercancel', onUp); el.addEventListener('pointerleave', onLeave); el.addEventListener('keydown', onKey);\n\n  const ro = new ResizeObserver(() => { resize(); kick(); }); ro.observe(el);\n  const io = new IntersectionObserver(([en]) => { S.visible = en.isIntersecting; if (S.visible) kick(); }, { rootMargin: '100px' }); io.observe(el);\n  const onVis = () => { if (!document.hidden) kick(); }; document.addEventListener('visibilitychange', onVis);\n  const onLost = (e) => { e.preventDefault(); cancelAnimationFrame(S.raf); S.raf = 0; S.visible = false; };\n  const onRestored = () => { destroy(); el.__vsRemount(); };\n  canvas.addEventListener('webglcontextlost', onLost); canvas.addEventListener('webglcontextrestored', onRestored);\n  function destroy() {\n    cancelAnimationFrame(S.raf); ro.disconnect(); io.disconnect(); document.removeEventListener('visibilitychange', onVis);\n    ['pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerleave', 'keydown'].forEach((t, i) => el.removeEventListener(t, [onDown, onMove, onUp, onUp, onLeave, onKey][i]));\n    canvas.removeEventListener('webglcontextlost', onLost); canvas.removeEventListener('webglcontextrestored', onRestored);\n    geo.dispose(); metal.dispose(); reflMat.dispose(); if (envRT) envRT.dispose(); pmrem.dispose(); renderer.dispose();\n  }\n  resize(); renderer.compile(scene, camera); kick();\n  return { set, destroy, spin: (v = 8) => { S.w += v; kick(); }, get rpm() { return S.w * 60 / (Math.PI * 2); }, get yaw() { return S.yaw; } };\n}\n\ndocument.querySelectorAll('.vs-chrome-logo:not([data-vs-mounted])').forEach((el) => {\n  el.dataset.vsMounted = '';\n  const go = () => { const api = mount(el); if (api) el.vsChromeLogo = api; };\n  el.__vsRemount = go; go();\n});\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "3D objects",
    "tags": [
      "chrome",
      "logo",
      "svg",
      "extrude",
      "metal",
      "reflection",
      "iridescent",
      "drag",
      "inertia"
    ],
    "tech": [
      "three.js",
      "GLSL",
      "JS",
      "CSS"
    ],
    "deps": [
      "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
      "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/loaders/SVGLoader.js",
      "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/utils/BufferGeometryUtils.js"
    ],
    "source": null,
    "free": true,
    "accent": "#5cc8ff",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/chrome-logo/index.html",
    "thumbnail": "https://vividsites.app/api/component/chrome-logo/thumb.jpg",
    "video": "https://vividsites.app/api/component/chrome-logo/thumb.mp4",
    "page": "https://vividsites.app/c/chrome-logo"
  }
}