{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rotating-product-stage",
  "type": "registry:ui",
  "title": "Rotating Product Stage",
  "description": "A CSS 3D product stage with drag-to-rotate inertia, keyboard turns and per-face lighting presets.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/rotating-product-stage.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\ntype FaceName = \"front\" | \"back\" | \"right\" | \"left\" | \"top\" | \"bottom\";\ntype Vec3 = [number, number, number];\nexport type StageLighting = \"studio\" | \"warm\" | \"night\";\n\nexport interface RotatingProductStageProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** Product name printed on the front face and used in the accessible name. */\n  title?: string;\n  /** Brand mark printed on the faces. */\n  brand?: string;\n  /** Description read by assistive technology. */\n  description?: string;\n  lighting?: StageLighting;\n  /** Slowly turn the product while idle. */\n  autoRotate?: boolean;\n  /** Auto-rotation speed in degrees per second. */\n  autoRotateSpeed?: number;\n  /** Keep spinning after a flick, with friction. */\n  inertia?: boolean;\n  /** CSS perspective distance in px — lower is more dramatic. */\n  perspective?: number;\n  /** Degrees of rotation per pixel dragged. */\n  sensitivity?: number;\n  accent?: string;\n  /** Show the reset button and the interaction hint. */\n  showControls?: boolean;\n  /** Resting angle in degrees. */\n  initialRotation?: { x: number; y: number };\n  /** Replace the content of individual faces. */\n  faces?: Partial<Record<FaceName, React.ReactNode>>;\n}\n\n/* ----------------------------- Lighting model ----------------------------- */\n\ninterface LightingPreset {\n  name: string;\n  /** Unit vector pointing toward the light (screen space: +x right, +y down, +z toward viewer). */\n  direction: Vec3;\n  ambient: number;\n  shadeStrength: number;\n  specular: number;\n  specularColor: string;\n  body: string;\n  ink: string;\n  muted: string;\n  stage: string;\n  shadow: string;\n}\n\nconst normalize = ([x, y, z]: Vec3): Vec3 => {\n  const l = Math.hypot(x, y, z) || 1;\n  return [x / l, y / l, z / l];\n};\n\nconst LIGHTING: Record<StageLighting, LightingPreset> = {\n  studio: {\n    name: \"Studio\",\n    direction: normalize([-0.45, -0.75, 0.62]),\n    ambient: 0.6,\n    shadeStrength: 0.6,\n    specular: 0.55,\n    specularColor: \"rgba(255,255,255,0.9)\",\n    body: \"#F1EFEA\",\n    ink: \"#17181A\",\n    muted: \"rgba(23,24,26,0.5)\",\n    stage:\n      \"bg-[radial-gradient(120%_90%_at_50%_0%,#ffffff_0%,#ECEAE4_52%,#D8D4CB_100%)] dark:bg-[radial-gradient(120%_90%_at_50%_0%,#33363b_0%,#1c1e21_55%,#111214_100%)]\",\n    shadow: \"rgba(20,18,14,0.38)\",\n  },\n  warm: {\n    name: \"Warm\",\n    direction: normalize([0.7, -0.45, 0.55]),\n    ambient: 0.42,\n    shadeStrength: 0.7,\n    specular: 0.65,\n    specularColor: \"rgba(255,214,170,0.95)\",\n    body: \"#E7D5BF\",\n    ink: \"#2A1D12\",\n    muted: \"rgba(42,29,18,0.5)\",\n    stage:\n      \"bg-[radial-gradient(110%_90%_at_80%_0%,#FFF3E4_0%,#F1D6B8_50%,#D9AE84_100%)] dark:bg-[radial-gradient(110%_90%_at_80%_0%,#4a3020_0%,#24170f_55%,#140d08_100%)]\",\n    shadow: \"rgba(70,38,12,0.42)\",\n  },\n  night: {\n    name: \"Night\",\n    direction: normalize([-0.65, -0.35, 0.45]),\n    ambient: 0.26,\n    shadeStrength: 0.8,\n    specular: 0.85,\n    specularColor: \"rgba(158,201,255,0.85)\",\n    body: \"#272A31\",\n    ink: \"#F1F2F4\",\n    muted: \"rgba(241,242,244,0.5)\",\n    stage: \"bg-[radial-gradient(110%_90%_at_20%_0%,#24304a_0%,#0e131d_55%,#06080c_100%)]\",\n    shadow: \"rgba(0,0,0,0.7)\",\n  },\n};\n\nconst FACE_NORMALS: Record<FaceName, Vec3> = {\n  front: [0, 0, 1],\n  back: [0, 0, -1],\n  right: [1, 0, 0],\n  left: [-1, 0, 0],\n  top: [0, -1, 0],\n  bottom: [0, 1, 0],\n};\nconst FACE_NAMES = Object.keys(FACE_NORMALS) as FaceName[];\nconst VIEW_HALF: Vec3 = [0, 0, 1];\n\n/** Applies CSS `rotateX(ax) rotateY(ay)` (rotateY first) to a vector. Angles in degrees. */\nfunction rotate([x, y, z]: Vec3, ax: number, ay: number): Vec3 {\n  const ry = (ay * Math.PI) / 180;\n  const rx = (ax * Math.PI) / 180;\n  const x1 = Math.cos(ry) * x + Math.sin(ry) * z;\n  const z1 = -Math.sin(ry) * x + Math.cos(ry) * z;\n  return [x1, Math.cos(rx) * y - Math.sin(rx) * z1, Math.sin(rx) * y + Math.cos(rx) * z1];\n}\n\nconst dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n\n/** Lambert diffuse + Blinn specular per face, expressed as overlay opacities. */\nfunction shadeFaces(ax: number, ay: number, light: LightingPreset) {\n  const half = normalize([light.direction[0] + VIEW_HALF[0], light.direction[1] + VIEW_HALF[1], light.direction[2] + VIEW_HALF[2]]);\n  const out = {} as Record<FaceName, { shade: number; spec: number }>;\n  for (const face of FACE_NAMES) {\n    const n = rotate(FACE_NORMALS[face], ax, ay);\n    const diffuse = Math.max(0, dot(n, light.direction));\n    const brightness = light.ambient + (1 - light.ambient) * diffuse;\n    out[face] = {\n      shade: Math.min(0.9, (1 - brightness) * light.shadeStrength),\n      spec: Math.pow(Math.max(0, dot(n, half)), 18) * light.specular,\n    };\n  }\n  return out;\n}\n\n/* ------------------------------ Geometry -------------------------------- */\n\n// Design units; the stage scales them with its container (1u = min(width, height) / 560).\nconst W = 200;\nconst H = 260;\nconst D = 120;\nconst u = (n: number) => `calc(var(--ps-u) * ${n})`;\n\nconst FACE_GEOMETRY: Record<FaceName, { w: number; h: number; transform: string }> = {\n  front: { w: W, h: H, transform: `translateZ(${u(D / 2)})` },\n  back: { w: W, h: H, transform: `rotateY(180deg) translateZ(${u(D / 2)})` },\n  right: { w: D, h: H, transform: `rotateY(90deg) translateZ(${u(W / 2)})` },\n  left: { w: D, h: H, transform: `rotateY(-90deg) translateZ(${u(W / 2)})` },\n  top: { w: W, h: D, transform: `rotateX(90deg) translateZ(${u(H / 2)})` },\n  bottom: { w: W, h: D, transform: `rotateX(-90deg) translateZ(${u(H / 2)})` },\n};\n\nconst MONO = \"font-mono uppercase\";\n\nfunction DefaultFace({ face, title, brand }: { face: FaceName; title: string; brand: string }) {\n  switch (face) {\n    case \"front\":\n      return (\n        <div className=\"flex h-full flex-col justify-between\" style={{ padding: u(16) }}>\n          <div className=\"flex items-center justify-between\">\n            <span className={MONO} style={{ fontSize: u(9), letterSpacing: \"0.18em\" }}>\n              {brand}\n            </span>\n            <span className=\"rounded-full bg-[var(--ps-accent)]\" style={{ width: u(7), height: u(7) }} />\n          </div>\n          <div\n            className=\"mx-auto rounded-full\"\n            style={{\n              width: u(132),\n              height: u(132),\n              backgroundImage: \"radial-gradient(circle, var(--ps-muted) 30%, transparent 36%)\",\n              backgroundSize: `${u(7)} ${u(7)}`,\n              boxShadow: \"inset 0 0 0 1px var(--ps-muted)\",\n            }}\n          />\n          <div>\n            <div className=\"font-semibold tracking-[-0.03em]\" style={{ fontSize: u(22), lineHeight: 1 }}>\n              {title}\n            </div>\n            <div className={`${MONO} mt-1 text-[var(--ps-muted)]`} style={{ fontSize: u(8), letterSpacing: \"0.16em\" }}>\n              Edition 2026 · Hand-finished\n            </div>\n          </div>\n        </div>\n      );\n    case \"back\":\n      return (\n        <div className=\"flex h-full flex-col justify-between\" style={{ padding: u(16) }}>\n          <div className={`${MONO} space-y-1 text-[var(--ps-muted)]`} style={{ fontSize: u(8), letterSpacing: \"0.14em\" }}>\n            <div>Serial · WC-01-0425</div>\n            <div>Input · 24V ⎓ 1.5A</div>\n            <div>Designed in Kuching</div>\n          </div>\n          <div className=\"flex items-center\" style={{ gap: u(8) }}>\n            {[18, 18, 30].map((w, i) => (\n              <span key={i} className=\"rounded-[2px] bg-black/50\" style={{ width: u(w), height: u(9) }} />\n            ))}\n          </div>\n        </div>\n      );\n    case \"left\":\n    case \"right\":\n      return (\n        <div className=\"flex h-full items-center justify-center\">\n          <span\n            className={`${MONO} text-[var(--ps-muted)]`}\n            style={{ fontSize: u(8), letterSpacing: \"0.3em\", writingMode: \"vertical-rl\", transform: face === \"left\" ? \"rotate(180deg)\" : undefined }}\n          >\n            {brand} · {title}\n          </span>\n          <span className=\"absolute bg-[var(--ps-accent)]\" style={{ width: u(2), height: u(40), bottom: u(18), left: \"50%\", marginLeft: u(-1) }} />\n        </div>\n      );\n    case \"top\":\n      return (\n        <div className=\"flex h-full items-center justify-center\">\n          <span\n            className=\"relative rounded-full\"\n            style={{\n              width: u(64),\n              height: u(64),\n              boxShadow: \"inset 0 0 0 1px var(--ps-muted), 0 1px 0 rgba(255,255,255,0.25)\",\n              background: \"radial-gradient(circle at 35% 30%, rgba(255,255,255,0.35), transparent 60%)\",\n            }}\n          >\n            <span className=\"absolute left-1/2 rounded-full bg-[var(--ps-accent)]\" style={{ width: u(3), height: u(14), top: u(6), marginLeft: u(-1.5) }} />\n          </span>\n        </div>\n      );\n    default:\n      return null;\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nconst clampTilt = (x: number) => Math.max(-70, Math.min(70, x));\nconst easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);\n\n/**\n * A CSS 3D product stage: drag (with inertia) or use the arrow keys to turn a lit, labelled box.\n * Per-face shading is computed from the light direction and current rotation, written to CSS\n * variables in a requestAnimationFrame loop that sleeps whenever nothing is moving.\n */\nexport function RotatingProductStage({\n  title = \"Model 01\",\n  brand = \"Wancraft\",\n  description = \"A matte compact speaker with a perforated front grille, a top-mounted dial and an accent status light.\",\n  lighting = \"studio\",\n  autoRotate = true,\n  autoRotateSpeed = 14,\n  inertia = true,\n  perspective = 1100,\n  sensitivity = 0.4,\n  accent = \"#FF7448\",\n  showControls = true,\n  initialRotation = { x: -16, y: -32 },\n  faces,\n  className = \"\",\n  style,\n  ...props\n}: RotatingProductStageProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const descId = React.useId();\n  const light = LIGHTING[lighting] ?? LIGHTING.studio;\n  const initX = initialRotation.x;\n  const initY = initialRotation.y;\n\n  const objectRef = React.useRef<HTMLDivElement>(null);\n  const rotation = React.useRef({ x: initX, y: initY, vx: 0, vy: 0 });\n  const dragRef = React.useRef<{ id: number; x: number; y: number; t: number } | null>(null);\n  const tweenRef = React.useRef<{ fx: number; fy: number; tx: number; ty: number; t0: number; dur: number } | null>(null);\n  const idleUntil = React.useRef(0);\n  const kickRef = React.useRef<(() => void) | null>(null);\n  const [dragging, setDragging] = React.useState(false);\n\n  // Initial shading for the server render; the loop takes over on the client.\n  const initialShade = shadeFaces(initX, initY, light);\n\n  // Layout effect: re-applies the live rotation before paint whenever lighting or settings change.\n  React.useLayoutEffect(() => {\n    const object = objectRef.current;\n    if (!object) return;\n    const faceEls = FACE_NAMES.map((name) => object.querySelector<HTMLElement>(`[data-face=\"${name}\"]`));\n    const animateIdle = autoRotate && !reduceMotion;\n    let frame = 0;\n    let running = false;\n    let visible = true;\n    let last = 0;\n\n    const apply = () => {\n      const { x, y } = rotation.current;\n      object.style.transform = `rotateX(${x.toFixed(2)}deg) rotateY(${y.toFixed(2)}deg)`;\n      const shading = shadeFaces(x, y, light);\n      faceEls.forEach((el, i) => {\n        if (!el) return;\n        const s = shading[FACE_NAMES[i]];\n        el.style.setProperty(\"--ps-shade\", s.shade.toFixed(3));\n        el.style.setProperty(\"--ps-spec\", s.spec.toFixed(3));\n      });\n    };\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(0.05, (now - last) / 1000) : 0;\n      last = now;\n      const r = rotation.current;\n      let active = false;\n\n      if (dragRef.current) {\n        active = true; // pointer handlers move the object; we only render\n      } else if (tweenRef.current) {\n        const tw = tweenRef.current;\n        const t = tw.dur <= 0 ? 1 : Math.min(1, (now - tw.t0) / tw.dur);\n        const e = easeOutCubic(t);\n        r.x = tw.fx + (tw.tx - tw.fx) * e;\n        r.y = tw.fy + (tw.ty - tw.fy) * e;\n        if (t >= 1) tweenRef.current = null;\n        active = true;\n      } else {\n        if (inertia && !reduceMotion && (Math.abs(r.vx) > 0.5 || Math.abs(r.vy) > 0.5)) {\n          r.y += r.vy * dt;\n          r.x = clampTilt(r.x + r.vx * dt);\n          const friction = Math.pow(0.035, dt); // keeps ~3.5% of velocity per second\n          r.vx *= friction;\n          r.vy *= friction;\n          active = true;\n        } else {\n          r.vx = 0;\n          r.vy = 0;\n        }\n        if (animateIdle) {\n          if (now > idleUntil.current) r.y += autoRotateSpeed * dt;\n          active = true;\n        }\n      }\n\n      apply();\n      if (active && visible && !document.hidden) {\n        frame = requestAnimationFrame(loop);\n      } else {\n        running = false;\n        last = 0;\n      }\n    };\n\n    const kick = () => {\n      if (running || !visible || document.hidden) return;\n      running = true;\n      last = 0;\n      frame = requestAnimationFrame(loop);\n    };\n    kickRef.current = kick;\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) kick();\n    });\n    io.observe(object.parentElement ?? object);\n    const onVisibility = () => {\n      if (!document.hidden) kick();\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    apply();\n    kick();\n    return () => {\n      cancelAnimationFrame(frame);\n      running = false;\n      kickRef.current = null;\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n    };\n  }, [light, autoRotate, autoRotateSpeed, inertia, reduceMotion]);\n\n  const tweenTo = (tx: number, ty: number, dur = 520) => {\n    const r = rotation.current;\n    const now = performance.now();\n    tweenRef.current = { fx: r.x, fy: r.y, tx: clampTilt(tx), ty, t0: now, dur: reduceMotion ? 0 : dur };\n    r.vx = 0;\n    r.vy = 0;\n    idleUntil.current = now + 2500;\n    kickRef.current?.();\n  };\n\n  const reset = () => {\n    const r = rotation.current;\n    // Return by the shortest way round instead of unwinding every turn.\n    const ty = initY + 360 * Math.round((r.y - initY) / 360);\n    tweenTo(initX, ty, 800);\n  };\n\n  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (event.button !== 0) return;\n    event.currentTarget.setPointerCapture(event.pointerId);\n    dragRef.current = { id: event.pointerId, x: event.clientX, y: event.clientY, t: event.timeStamp };\n    tweenRef.current = null;\n    rotation.current.vx = 0;\n    rotation.current.vy = 0;\n    setDragging(true);\n    kickRef.current?.();\n  };\n\n  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    const drag = dragRef.current;\n    if (!drag || drag.id !== event.pointerId) return;\n    const dx = event.clientX - drag.x;\n    const dy = event.clientY - drag.y;\n    const dt = Math.max(1, event.timeStamp - drag.t) / 1000;\n    const r = rotation.current;\n    r.y += dx * sensitivity;\n    r.x = clampTilt(r.x - dy * sensitivity);\n    // Smoothed velocity in degrees per second, used for the inertial flick.\n    r.vy = r.vy * 0.6 + ((dx * sensitivity) / dt) * 0.4;\n    r.vx = r.vx * 0.6 + ((-dy * sensitivity) / dt) * 0.4;\n    dragRef.current = { id: drag.id, x: event.clientX, y: event.clientY, t: event.timeStamp };\n  };\n\n  const endDrag = (event: React.PointerEvent<HTMLDivElement>) => {\n    const drag = dragRef.current;\n    if (!drag || drag.id !== event.pointerId) return;\n    // A pause before release means no flick.\n    if (event.timeStamp - drag.t > 90) {\n      rotation.current.vx = 0;\n      rotation.current.vy = 0;\n    }\n    dragRef.current = null;\n    idleUntil.current = performance.now() + 2500;\n    setDragging(false);\n    kickRef.current?.();\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const r = rotation.current;\n    const base = tweenRef.current ? { x: tweenRef.current.tx, y: tweenRef.current.ty } : { x: r.x, y: r.y };\n    const step = event.shiftKey ? 90 : 30;\n    const actions: Record<string, () => void> = {\n      ArrowLeft: () => tweenTo(base.x, base.y - step),\n      ArrowRight: () => tweenTo(base.x, base.y + step),\n      ArrowUp: () => tweenTo(base.x - 15, base.y),\n      ArrowDown: () => tweenTo(base.x + 15, base.y),\n      Home: reset,\n    };\n    const action = actions[event.key];\n    if (!action) return;\n    event.preventDefault();\n    action();\n  };\n\n  const accessibleName = `${brand} ${title}`;\n\n  return (\n    <div\n      className={`relative h-full min-h-[260px] w-full overflow-hidden [container-type:size] ${light.stage} ${className}`}\n      style={{ [\"--ps-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      <div\n        tabIndex={0}\n        role=\"group\"\n        aria-roledescription=\"3D product viewer\"\n        aria-label={accessibleName}\n        aria-describedby={descId}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={endDrag}\n        onPointerCancel={endDrag}\n        onKeyDown={handleKeyDown}\n        className={`absolute inset-0 touch-pan-y select-none outline-none focus-visible:shadow-[inset_0_0_0_2px_var(--ps-accent)] ${\n          dragging ? \"cursor-grabbing\" : \"cursor-grab\"\n        }`}\n        style={{\n          [\"--ps-u\" as string]: \"calc(min(100cqw, 100cqh) / 560)\",\n          [\"--ps-ink\" as string]: light.ink,\n          [\"--ps-muted\" as string]: light.muted,\n        }}\n      >\n        {/* Contact shadow */}\n        <div\n          aria-hidden=\"true\"\n          className=\"absolute left-1/2 top-1/2 -translate-x-1/2 rounded-[50%] blur-[14px]\"\n          style={{ width: u(260), height: u(46), marginTop: u(150), background: `radial-gradient(closest-side, ${light.shadow}, transparent)` }}\n        />\n        <div\n          role=\"img\"\n          aria-label={description}\n          className=\"absolute inset-0 grid place-items-center\"\n          style={{ perspective: `${perspective}px` }}\n        >\n          <div\n            ref={objectRef}\n            aria-hidden=\"true\"\n            className=\"relative [transform-style:preserve-3d]\"\n            style={{ width: 0, height: 0, transform: `rotateX(${initX}deg) rotateY(${initY}deg)` }}\n          >\n            {FACE_NAMES.map((face) => {\n              const g = FACE_GEOMETRY[face];\n              return (\n                <div\n                  key={face}\n                  data-face={face}\n                  className=\"absolute left-0 top-0 overflow-hidden text-[var(--ps-ink)] [backface-visibility:hidden]\"\n                  style={{\n                    width: u(g.w),\n                    height: u(g.h),\n                    marginLeft: u(-g.w / 2),\n                    marginTop: u(-g.h / 2),\n                    transform: g.transform,\n                    backgroundColor: light.body,\n                    borderRadius: u(face === \"front\" || face === \"back\" ? 6 : 3),\n                    boxShadow: \"inset 0 0 0 1px rgba(255,255,255,0.18), inset 0 0 0 2px rgba(0,0,0,0.04)\",\n                    [\"--ps-shade\" as string]: initialShade[face].shade.toFixed(3),\n                    [\"--ps-spec\" as string]: initialShade[face].spec.toFixed(3),\n                  }}\n                >\n                  {faces?.[face] ?? <DefaultFace face={face} title={title} brand={brand} />}\n                  {/* Diffuse shading and specular highlight, driven by CSS variables. */}\n                  <div className=\"pointer-events-none absolute inset-0 bg-black\" style={{ opacity: \"var(--ps-shade)\" }} />\n                  <div\n                    className=\"pointer-events-none absolute inset-0 mix-blend-screen\"\n                    style={{\n                      opacity: \"var(--ps-spec)\",\n                      background: `radial-gradient(120% 90% at 30% 15%, ${light.specularColor}, transparent 65%)`,\n                    }}\n                  />\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n\n      <p id={descId} className=\"sr-only\">\n        {`${description} Drag, or use the arrow keys, to rotate. Hold Shift for quarter turns. Press Home to reset the view.`}\n      </p>\n\n      {showControls ? (\n        <>\n          <div\n            aria-hidden=\"true\"\n            className={`pointer-events-none absolute left-4 top-4 font-mono text-[10px] uppercase tracking-[0.14em] ${\n              lighting === \"night\" ? \"text-white/60\" : \"text-zinc-900/55 dark:text-white/55\"\n            }`}\n          >\n            {light.name} light\n          </div>\n          <div\n            aria-hidden=\"true\"\n            className={`pointer-events-none absolute bottom-4 left-4 font-mono text-[10px] uppercase tracking-[0.14em] ${\n              lighting === \"night\" ? \"text-white/50\" : \"text-zinc-900/45 dark:text-white/45\"\n            }`}\n          >\n            Drag · ← → ↑ ↓\n          </div>\n          <button\n            type=\"button\"\n            onClick={reset}\n            className={`absolute bottom-3 right-3 inline-flex h-8 items-center gap-1.5 rounded-full border px-3 font-mono text-[10px] uppercase tracking-[0.14em] backdrop-blur-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ps-accent)] ${\n              lighting === \"night\"\n                ? \"border-white/15 bg-white/5 text-white/80 hover:border-white/40 hover:text-white\"\n                : \"border-zinc-900/15 bg-white/50 text-zinc-700 hover:border-zinc-900/40 hover:text-zinc-950 dark:border-white/15 dark:bg-white/5 dark:text-white/80 dark:hover:border-white/40 dark:hover:text-white\"\n            }`}\n          >\n            <svg viewBox=\"0 0 12 12\" className=\"size-3\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.3\" aria-hidden=\"true\">\n              <path d=\"M2.2 6a3.8 3.8 0 1 0 1.1-2.7\" strokeLinecap=\"round\" />\n              <path d=\"M2 1.8v2.2h2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n            Reset view\n          </button>\n        </>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "3d"
  ],
  "docs": "Documentation: https://wancraft.my/components/rotating-product-stage"
}
