{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "world-cup",
  "title": "WorldCup",
  "description": "The World Cup trophy modeled procedurally: two sculpted figures with raised arms lift a continent-embossed globe above a green-banded base, turning under studio light amid a drift of confetti.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "files": [
    {
      "path": "components/threecn/world-cup.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useFrame } from \"@react-three/fiber\"\nimport { Environment, Lightformer } from \"@react-three/drei\"\nimport * as THREE from \"three\"\n\nimport {\n  SceneContainer,\n  type SceneContainerProps,\n} from \"@/components/threecn/scene-container\"\nimport {\n  useShadcnTheme,\n  type ThemeMode,\n} from \"@/components/hooks/use-shadcn-theme\"\n\n/* ------------------------------------------------------------------ */\n/* Proportions (scene units). The real trophy: a draped column pinched */\n/* at the waist, two figures with raised arms, a globe resting in the  */\n/* upstretched hands, all on a stepped base with two green bands.      */\n/* ------------------------------------------------------------------ */\n\nconst GLOBE_R = 0.62\nconst GLOBE_Y = 2.45 // globe center; its lower third nests into the hands\nconst BODY_TOP = 2.05\n/** Vertical shift so the whole piece sits centered on the origin. */\nconst CENTER_Y = -1.28\n\n/**\n * Silhouette of the body as (radius, height) control points, bottom to top:\n * a draped skirt narrowing to the waist, then swelling through the figures'\n * chests and opening where the arms rise to meet the globe.\n */\nconst PROFILE: [number, number][] = [\n  [0.45, 0.0],\n  [0.36, 0.1],\n  [0.29, 0.28],\n  [0.24, 0.52],\n  [0.215, 0.85], // waist\n  [0.24, 1.15],\n  [0.3, 1.4],\n  [0.38, 1.62],\n  [0.45, 1.8],\n  [0.49, 1.95],\n  [0.45, BODY_TOP], // curls back in right under the globe\n]\n\nconst BODY_ROWS = 200\nconst BODY_COLS = 160\n\n/* ----------------------------- noise ------------------------------ */\n\nfunction hash3(x: number, y: number, z: number) {\n  const s = Math.sin(x * 127.1 + y * 311.7 + z * 74.7) * 43758.5453\n  return s - Math.floor(s)\n}\n\n/** Trilinear value noise in [0, 1]. */\nfunction vnoise3(x: number, y: number, z: number) {\n  const ix = Math.floor(x)\n  const iy = Math.floor(y)\n  const iz = Math.floor(z)\n  const fx = x - ix\n  const fy = y - iy\n  const fz = z - iz\n  const ux = fx * fx * (3 - 2 * fx)\n  const uy = fy * fy * (3 - 2 * fy)\n  const uz = fz * fz * (3 - 2 * fz)\n  const lerp = (a: number, b: number, t: number) => a + (b - a) * t\n  const c00 = lerp(hash3(ix, iy, iz), hash3(ix + 1, iy, iz), ux)\n  const c10 = lerp(hash3(ix, iy + 1, iz), hash3(ix + 1, iy + 1, iz), ux)\n  const c01 = lerp(hash3(ix, iy, iz + 1), hash3(ix + 1, iy, iz + 1), ux)\n  const c11 = lerp(hash3(ix, iy + 1, iz + 1), hash3(ix + 1, iy + 1, iz + 1), ux)\n  return lerp(lerp(c00, c10, uy), lerp(c01, c11, uy), uz)\n}\n\nfunction fbm3(x: number, y: number, z: number) {\n  let sum = 0\n  let amp = 0.5\n  let freq = 1\n  for (let o = 0; o < 4; o++) {\n    sum += amp * vnoise3(x * freq + o * 7.3, y * freq + o * 3.1, z * freq)\n    amp *= 0.5\n    freq *= 2.1\n  }\n  return sum // ≈ [0, 1]\n}\n\nconst smoothstep = (e0: number, e1: number, x: number) => {\n  const t = Math.min(1, Math.max(0, (x - e0) / (e1 - e0)))\n  return t * t * (3 - 2 * t)\n}\n\n/* --------------------------- geometry ----------------------------- */\n\n/**\n * The draped body. On top of the lathe profile: a front/back swell for the\n * two figures (they stand back to back along ±z), spiraling drapery folds,\n * and fine sculptural noise so the gold reads as cast metal, not plastic.\n */\nfunction buildBody(): THREE.BufferGeometry {\n  const curve = new THREE.CatmullRomCurve3(\n    PROFILE.map(([r, y]) => new THREE.Vector3(r, y, 0))\n  )\n  const positions = new Float32Array(BODY_ROWS * BODY_COLS * 3)\n\n  for (let i = 0; i < BODY_ROWS; i++) {\n    const t = i / (BODY_ROWS - 1)\n    const p = curve.getPoint(t)\n    const r0 = p.x\n    const y = p.y\n\n    // Figures' torsos: strongest at chest height, gone at foot and rim.\n    const figEnv = Math.exp(-Math.pow((y - 1.35) / 0.5, 2))\n    // Drapery folds live on the skirt and waist, fading toward the arms.\n    const foldEnv =\n      smoothstep(0.02, 0.2, y) * (1 - 0.75 * smoothstep(1.2, 1.9, y))\n\n    for (let j = 0; j < BODY_COLS; j++) {\n      const a = (j / BODY_COLS) * Math.PI * 2\n      const sa = Math.sin(a)\n\n      // Two-lobed cross-section → the figures' bodies front and back.\n      let r = r0 * (1 + 0.18 * figEnv * sa * sa)\n      // Spiraling folds (two frequencies, opposite twists) + cast-metal grain.\n      r +=\n        foldEnv *\n        (0.038 * Math.sin(9 * a + y * 1.6 + 0.8) +\n          0.022 * Math.sin(15 * a - y * 2.2 + 3.1))\n      r +=\n        foldEnv *\n        0.03 *\n        (fbm3(Math.cos(a) * 1.8, y * 1.3, sa * 1.8) - 0.5)\n\n      const k = (i * BODY_COLS + j) * 3\n      positions[k] = r * Math.cos(a)\n      positions[k + 1] = y\n      positions[k + 2] = r * sa\n    }\n  }\n\n  const index: number[] = []\n  for (let i = 0; i < BODY_ROWS - 1; i++) {\n    for (let j = 0; j < BODY_COLS; j++) {\n      const a = i * BODY_COLS + j\n      const b = i * BODY_COLS + ((j + 1) % BODY_COLS)\n      const c = (i + 1) * BODY_COLS + j\n      const d = (i + 1) * BODY_COLS + ((j + 1) % BODY_COLS)\n      index.push(a, c, b, b, c, d)\n    }\n  }\n\n  const geo = new THREE.BufferGeometry()\n  geo.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3))\n  geo.setIndex(index)\n  geo.computeVertexNormals()\n  return geo\n}\n\n/**\n * The globe: continents raised in relief from procedural noise, oceans\n * hatched with fine latitude striations — like the engraved original.\n */\nfunction buildGlobe(): THREE.BufferGeometry {\n  const geo = new THREE.SphereGeometry(GLOBE_R, 144, 104)\n  const pos = geo.attributes.position as THREE.BufferAttribute\n  const dir = new THREE.Vector3()\n  for (let i = 0; i < pos.count; i++) {\n    dir.fromBufferAttribute(pos, i).normalize()\n    const land = fbm3(dir.x * 1.6 + 4.2, dir.y * 1.6, dir.z * 1.6 + 9.7)\n    const mask = smoothstep(0.5, 0.57, land)\n    const relief =\n      mask * (0.035 + 0.04 * fbm3(dir.x * 6, dir.y * 6 + 2.4, dir.z * 6))\n    const lat = Math.asin(Math.min(1, Math.max(-1, dir.y)))\n    const hatch = (1 - mask) * 0.0045 * Math.sin(lat * 46)\n    const r = GLOBE_R + relief + hatch\n    pos.setXYZ(i, dir.x * r, dir.y * r, dir.z * r)\n  }\n  geo.computeVertexNormals()\n  return geo\n}\n\n/**\n * One raised arm as a curve from shoulder up to the globe's widest flank,\n * so the hands visibly cradle the ball like the original.\n */\nfunction armCurve(sx: number, sz: number): THREE.CatmullRomCurve3 {\n  const ex = Math.sign(sx) * 0.585\n  return new THREE.CatmullRomCurve3([\n    new THREE.Vector3(sx, 1.48, sz),\n    new THREE.Vector3(sx * 1.8, 1.85, sz * 0.9),\n    new THREE.Vector3(ex * 1.02, 2.18, sz * 0.55),\n    new THREE.Vector3(ex, 2.42, sz * 0.5),\n  ])\n}\n\n/** Shoulder anchors: front figure (+z) and back figure (−z), two arms each. */\nconst ARMS: [number, number][] = [\n  [-0.26, 0.2],\n  [0.26, 0.2],\n  [-0.26, -0.2],\n  [0.26, -0.2],\n]\n\n/** Stepped base: gold foot, wide green band, gold ring, green band, gold cap. */\nconst BASE_STEPS: {\n  yTop: number\n  h: number\n  rTop: number\n  rBottom: number\n  green: boolean\n}[] = [\n  { yTop: -0.44, h: 0.08, rTop: 0.72, rBottom: 0.82, green: false },\n  { yTop: -0.28, h: 0.16, rTop: 0.6, rBottom: 0.72, green: true },\n  { yTop: -0.2, h: 0.08, rTop: 0.57, rBottom: 0.6, green: false },\n  { yTop: -0.08, h: 0.12, rTop: 0.5, rBottom: 0.57, green: true },\n  { yTop: 0.02, h: 0.1, rTop: 0.44, rBottom: 0.5, green: false },\n]\n\n/* --------------------------- confetti ----------------------------- */\n\nfunction Confetti({\n  count,\n  speed,\n  colors,\n}: {\n  count: number\n  speed: number\n  colors: THREE.Color[]\n}) {\n  const ref = React.useRef<THREE.Points>(null)\n\n  const { positions, colorBuf, vy, sway } = React.useMemo(() => {\n    const positions = new Float32Array(count * 3)\n    const colorBuf = new Float32Array(count * 3)\n    const vy = new Float32Array(count)\n    const sway = new Float32Array(count)\n    for (let i = 0; i < count; i++) {\n      // Deterministic scatter (no Math.random — keeps SSR/replay stable).\n      const h = (n: number) => {\n        const s = Math.sin(n) * 43758.5453\n        return s - Math.floor(s)\n      }\n      positions[i * 3] = (h(i * 1.7) - 0.5) * 4.2\n      positions[i * 3 + 1] = h(i * 2.3) * 5 - 0.5\n      positions[i * 3 + 2] = (h(i * 3.1) - 0.5) * 3\n      const c = colors[i % colors.length]\n      colorBuf[i * 3] = c.r\n      colorBuf[i * 3 + 1] = c.g\n      colorBuf[i * 3 + 2] = c.b\n      vy[i] = 0.35 + h(i * 5.9) * 0.6\n      sway[i] = h(i * 7.3) * Math.PI * 2\n    }\n    return { positions, colorBuf, vy, sway }\n  }, [count, colors])\n\n  useFrame(({ clock }, delta) => {\n    const pts = ref.current\n    if (!pts) return\n    const t = clock.getElapsedTime()\n    const attr = pts.geometry.attributes.position as THREE.BufferAttribute\n    const arr = attr.array as Float32Array\n    for (let i = 0; i < count; i++) {\n      let y = arr[i * 3 + 1] - vy[i] * speed * delta\n      if (y < -2.4) y = 4.6\n      arr[i * 3 + 1] = y\n      arr[i * 3] += Math.sin(t * 1.5 + sway[i]) * 0.004 * speed\n    }\n    attr.needsUpdate = true\n  })\n\n  return (\n    <points key={count} ref={ref} frustumCulled={false}>\n      <bufferGeometry>\n        <bufferAttribute attach=\"attributes-position\" args={[positions, 3]} />\n        <bufferAttribute attach=\"attributes-color\" args={[colorBuf, 3]} />\n      </bufferGeometry>\n      <pointsMaterial size={0.05} vertexColors sizeAttenuation transparent opacity={0.9} />\n    </points>\n  )\n}\n\n/* ---------------------------- trophy ------------------------------ */\n\nfunction Trophy({\n  speed,\n  gold,\n  confetti,\n  theme,\n}: {\n  speed: number\n  gold: string\n  confetti: number\n  theme: ThemeMode\n}) {\n  const { primaryColor, accentColor, foregroundColor, bgColor } =\n    useShadcnTheme(theme)\n  const groupRef = React.useRef<THREE.Group>(null)\n\n  const bodyGeo = React.useMemo(() => buildBody(), [])\n  const globeGeo = React.useMemo(() => buildGlobe(), [])\n  const armGeos = React.useMemo(\n    () =>\n      ARMS.map(\n        ([sx, sz]) => new THREE.TubeGeometry(armCurve(sx, sz), 28, 0.065, 10)\n      ),\n    []\n  )\n  const hands = React.useMemo(\n    () => ARMS.map(([sx, sz]) => armCurve(sx, sz).getPoint(1)),\n    []\n  )\n  React.useEffect(() => {\n    return () => {\n      bodyGeo.dispose()\n      globeGeo.dispose()\n      armGeos.forEach((g) => g.dispose())\n    }\n  }, [bodyGeo, globeGeo, armGeos])\n\n  const goldColor = React.useMemo(() => new THREE.Color(gold), [gold])\n  // Emerald bands, nudged toward the theme background so the trophy settles\n  // on the page instead of floating.\n  const greenColor = React.useMemo(\n    () => new THREE.Color(\"#0e6b3d\").lerp(bgColor, 0.08),\n    [bgColor]\n  )\n  const confettiColors = React.useMemo(\n    () => [\n      goldColor,\n      primaryColor.clone(),\n      accentColor.clone(),\n      foregroundColor.clone(),\n    ],\n    [goldColor, primaryColor, accentColor, foregroundColor]\n  )\n\n  const goldMat = React.useMemo(\n    () =>\n      new THREE.MeshStandardMaterial({\n        metalness: 1,\n        roughness: 0.28,\n        envMapIntensity: 1.2,\n        emissiveIntensity: 0.05,\n        side: THREE.DoubleSide,\n      }),\n    []\n  )\n  const greenMat = React.useMemo(\n    () =>\n      new THREE.MeshStandardMaterial({\n        metalness: 0.25,\n        roughness: 0.35,\n        envMapIntensity: 0.45,\n      }),\n    []\n  )\n  React.useEffect(() => {\n    goldMat.color.copy(goldColor)\n    goldMat.emissive.copy(goldColor)\n    greenMat.color.copy(greenColor)\n  }, [goldMat, greenMat, goldColor, greenColor])\n  React.useEffect(() => {\n    return () => {\n      goldMat.dispose()\n      greenMat.dispose()\n    }\n  }, [goldMat, greenMat])\n\n  useFrame(({ clock }) => {\n    const g = groupRef.current\n    if (!g) return\n    const t = clock.getElapsedTime()\n    g.rotation.y = t * 0.3 * speed\n    g.position.y = CENTER_Y + Math.sin(t * 0.8) * 0.04\n  })\n\n  return (\n    <>\n      {/* Local, CDN-free reflections so the gold reads as metal. */}\n      <Environment resolution={128} frames={1}>\n        <Lightformer\n          intensity={2.4}\n          position={[0, 3, 3]}\n          scale={[7, 7, 1]}\n          color=\"#fff6e0\"\n        />\n        <Lightformer\n          intensity={1.4}\n          position={[-4, 1, -2]}\n          scale={[5, 5, 1]}\n          color=\"#ffd98a\"\n        />\n        <Lightformer\n          intensity={1}\n          position={[4, -1, 1]}\n          scale={[4, 4, 1]}\n          color=\"#ffffff\"\n        />\n      </Environment>\n\n      <group ref={groupRef} position={[0, CENTER_Y, 0]}>\n        {/* Draped body with the two figures. */}\n        <mesh geometry={bodyGeo} material={goldMat} />\n\n        {/* Raised arms and hands cradling the globe. */}\n        {armGeos.map((geo, i) => (\n          <mesh key={i} geometry={geo} material={goldMat} />\n        ))}\n        {hands.map((p, i) => (\n          <mesh key={i} position={p} material={goldMat}>\n            <sphereGeometry args={[0.085, 16, 12]} />\n          </mesh>\n        ))}\n\n        {/* Continent-embossed globe. */}\n        <mesh geometry={globeGeo} material={goldMat} position={[0, GLOBE_Y, 0]} />\n\n        {/* Stepped base: gold foot, two green bands, gold ring between. */}\n        {BASE_STEPS.map((s, i) => (\n          <mesh\n            key={i}\n            position={[0, s.yTop - s.h / 2, 0]}\n            material={s.green ? greenMat : goldMat}\n          >\n            <cylinderGeometry args={[s.rTop, s.rBottom, s.h, 72]} />\n          </mesh>\n        ))}\n      </group>\n\n      {confetti > 0 ? (\n        <Confetti count={confetti} speed={speed} colors={confettiColors} />\n      ) : null}\n    </>\n  )\n}\n\nexport type WorldCupProps = {\n  /** Rotation and confetti speed multiplier. */\n  speed?: number\n  /** Color of the gold. */\n  gold?: string\n  /** Number of falling confetti particles (0 disables them). */\n  confetti?: number\n  className?: string\n  theme?: ThemeMode\n  environment?: SceneContainerProps[\"environment\"]\n}\n\n/**\n * The World Cup trophy, modeled procedurally after the original: two sculpted\n * figures rising back to back from a draped, spiraling column, arms raised to\n * hold a continent-embossed globe, on a stepped base with two green bands.\n * It turns slowly under studio light amid a drift of confetti, the confetti\n * and base tinted from your shadcn theme.\n */\nexport function WorldCup({\n  speed = 1,\n  gold = \"#e3b23c\",\n  confetti = 90,\n  className,\n  theme = \"auto\",\n  environment = \"studio\",\n}: WorldCupProps) {\n  return (\n    <SceneContainer\n      className={className}\n      theme={theme}\n      environment={environment}\n      camera={[0, 0.35, 7]}\n      fov={40}\n    >\n      <Trophy speed={speed} gold={gold} confetti={confetti} theme={theme} />\n    </SceneContainer>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "components/threecn/scene-container.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Canvas } from \"@react-three/fiber\"\nimport { OrbitControls } from \"@react-three/drei\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  useShadcnTheme,\n  type ThemeMode,\n} from \"@/components/hooks/use-shadcn-theme\"\n\nexport type EnvironmentPreset = \"studio\" | \"city\" | \"dawn\" | \"night\"\n\ntype LightRig = {\n  ambient: { color: string; intensity: number }\n  key: { color: string; intensity: number; position: [number, number, number] }\n  fill: { color: string; intensity: number; position: [number, number, number] }\n  rim: { color: string; intensity: number; position: [number, number, number] }\n}\n\n/**\n * Offline lighting rigs. We avoid drei's <Environment preset> because it\n * fetches HDRIs from a CDN — scenes must work with zero external assets.\n */\nconst RIGS: Record<EnvironmentPreset, LightRig> = {\n  studio: {\n    ambient: { color: \"#ffffff\", intensity: 0.6 },\n    key: { color: \"#ffffff\", intensity: 2.2, position: [5, 6, 5] },\n    fill: { color: \"#dfe3ff\", intensity: 0.8, position: [-6, 2, 4] },\n    rim: { color: \"#ffffff\", intensity: 1.4, position: [0, 4, -6] },\n  },\n  city: {\n    ambient: { color: \"#c7d2fe\", intensity: 0.7 },\n    key: { color: \"#e0e7ff\", intensity: 1.8, position: [4, 7, 4] },\n    fill: { color: \"#a5b4fc\", intensity: 0.9, position: [-5, 1, 5] },\n    rim: { color: \"#818cf8\", intensity: 1.2, position: [-2, 3, -6] },\n  },\n  dawn: {\n    ambient: { color: \"#fde2c8\", intensity: 0.5 },\n    key: { color: \"#ffb27a\", intensity: 2.0, position: [6, 4, 4] },\n    fill: { color: \"#9fb4ff\", intensity: 0.7, position: [-5, 2, 4] },\n    rim: { color: \"#ffd9a8\", intensity: 1.0, position: [0, 5, -5] },\n  },\n  night: {\n    ambient: { color: \"#1e1b4b\", intensity: 0.4 },\n    key: { color: \"#a78bfa\", intensity: 1.6, position: [4, 5, 4] },\n    fill: { color: \"#4338ca\", intensity: 0.6, position: [-5, 1, 4] },\n    rim: { color: \"#7c3aed\", intensity: 1.6, position: [-2, 3, -6] },\n  },\n}\n\nfunction Rig({\n  preset,\n  fog,\n  theme,\n}: {\n  preset: EnvironmentPreset\n  fog: boolean\n  theme: ThemeMode\n}) {\n  const { bgColor } = useShadcnTheme(theme)\n  const rig = RIGS[preset]\n\n  return (\n    <>\n      {fog ? <fog attach=\"fog\" args={[bgColor.getHex(), 8, 24]} /> : null}\n      <ambientLight color={rig.ambient.color} intensity={rig.ambient.intensity} />\n      <directionalLight\n        color={rig.key.color}\n        intensity={rig.key.intensity}\n        position={rig.key.position}\n      />\n      <directionalLight\n        color={rig.fill.color}\n        intensity={rig.fill.intensity}\n        position={rig.fill.position}\n      />\n      <pointLight\n        color={rig.rim.color}\n        intensity={rig.rim.intensity * 12}\n        position={rig.rim.position}\n        distance={30}\n      />\n    </>\n  )\n}\n\n/**\n * Track whether an element is near the viewport. WebGL contexts are a scarce\n * resource (browsers cap them at ~8-16), so scenes only hold one while they are\n * on (or close to) screen. The generous `rootMargin` pre-mounts a scene just\n * before it scrolls into view, avoiding a blank first frame.\n */\nfunction useInView(\n  ref: React.RefObject<HTMLElement | null>,\n  rootMargin: string\n): boolean {\n  const [inView, setInView] = React.useState(false)\n\n  React.useEffect(() => {\n    const el = ref.current\n    if (!el) return\n    if (typeof IntersectionObserver === \"undefined\") {\n      // No observer available: render eagerly rather than never mounting.\n      // eslint-disable-next-line react-hooks/set-state-in-effect\n      setInView(true)\n      return\n    }\n    const observer = new IntersectionObserver(\n      ([entry]) => setInView(entry.isIntersecting),\n      { rootMargin }\n    )\n    observer.observe(el)\n    return () => observer.disconnect()\n  }, [ref, rootMargin])\n\n  return inView\n}\n\nexport type SceneContainerProps = {\n  className?: string\n  theme?: ThemeMode\n  environment?: EnvironmentPreset\n  fog?: boolean\n  /** Camera position, defaults to a gentle 3/4 view. */\n  camera?: [number, number, number]\n  /** Vertical field of view. */\n  fov?: number\n  children?: React.ReactNode\n  /** Rendered as an HTML overlay above the canvas (not inside the 3D scene). */\n  overlay?: React.ReactNode\n  /**\n   * Drag to orbit the camera (rotate only — zoom and pan disabled). Defaults to\n   * true. Set false if the scene provides its own controls.\n   */\n  orbit?: boolean\n  /**\n   * Mount the WebGL canvas only while it is near the viewport, and tear it down\n   * once it scrolls away. Keeps a grid of many scenes from exhausting the\n   * browser's WebGL context limit and stops `useFrame` from running offscreen.\n   * Defaults to true. Set false for a scene that must always render (e.g. a\n   * persistent hero background).\n   */\n  lazy?: boolean\n}\n\n/**\n * The canvas wrapper every threecn scene builds on. Sets up a transparent\n * canvas (so your CSS background shows through), responsive DPR, a themed\n * lighting rig and optional fog.\n */\nexport function SceneContainer({\n  className,\n  theme = \"auto\",\n  environment = \"studio\",\n  fog = false,\n  camera = [0, 0, 6],\n  fov = 45,\n  children,\n  overlay,\n  orbit = true,\n  lazy = true,\n}: SceneContainerProps) {\n  const wrapRef = React.useRef<HTMLDivElement>(null)\n  const inView = useInView(wrapRef, \"300px\")\n  const active = lazy ? inView : true\n\n  return (\n    <div ref={wrapRef} className={cn(\"relative h-full w-full\", className)}>\n      {active ? (\n        <Canvas\n          dpr={[1, 2]}\n          gl={{ antialias: true, alpha: true, powerPreference: \"high-performance\" }}\n          camera={{ position: camera, fov, near: 0.1, far: 100 }}\n        >\n          <Rig preset={environment} fog={fog} theme={theme} />\n          <React.Suspense fallback={null}>{children}</React.Suspense>\n          {orbit ? (\n            <OrbitControls\n              makeDefault\n              enablePan={false}\n              enableZoom={false}\n              enableDamping\n              dampingFactor={0.1}\n            />\n          ) : null}\n        </Canvas>\n      ) : null}\n      {overlay ? <div className=\"pointer-events-none absolute inset-0\">{overlay}</div> : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "components/hooks/use-shadcn-theme.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Color } from \"three\"\n\n/**\n * The set of theme-derived colors exposed to R3F scenes.\n * Every value is a live `THREE.Color`, ready to assign to a material.\n */\nexport type ShadcnThemeColors = {\n  primaryColor: Color\n  primaryForegroundColor: Color\n  bgColor: Color\n  foregroundColor: Color\n  borderColor: Color\n  mutedColor: Color\n  accentColor: Color\n  isDark: boolean\n}\n\n/** CSS custom properties read from `:root` / `.dark`. */\nconst CSS_VARS = {\n  primaryColor: \"--primary\",\n  primaryForegroundColor: \"--primary-foreground\",\n  bgColor: \"--background\",\n  foregroundColor: \"--foreground\",\n  borderColor: \"--border\",\n  mutedColor: \"--muted-foreground\",\n  accentColor: \"--accent\",\n} as const\n\n/**\n * Neutral SSR defaults (a dark-ish indigo palette) used before the DOM is\n * available, so the first paint is never a flash of pure black.\n */\nfunction createDefaults(): ShadcnThemeColors {\n  return {\n    primaryColor: new Color().setHSL(263 / 360, 0.7, 0.6),\n    primaryForegroundColor: new Color().setHSL(0, 0, 0.98),\n    bgColor: new Color().setHSL(240 / 360, 0.1, 0.039),\n    foregroundColor: new Color().setHSL(0, 0, 0.98),\n    borderColor: new Color().setHSL(240 / 360, 0.037, 0.159),\n    mutedColor: new Color().setHSL(240 / 360, 0.05, 0.649),\n    accentColor: new Color().setHSL(240 / 360, 0.037, 0.159),\n    isDark: true,\n  }\n}\n\n/** Matches a bare shadcn HSL triplet, e.g. \"263 70% 50%\" (optional \"/ alpha\"). */\nconst HSL_TRIPLET = /^-?[\\d.]+\\s+-?[\\d.]+%\\s+-?[\\d.]+%(\\s*\\/\\s*[\\d.]+%?)?$/\n\n/**\n * A lazily-created 1x1 canvas 2D context used to normalise arbitrary CSS\n * colors. The browser's own color parser resolves ANY format the CSS spec\n * supports — including `oklch()` (Tailwind v4 / recent shadcn), `color()`,\n * `hsl()`, `rgb()`, hex and named colors — and serialises the result back as\n * `#rrggbb` / `rgba(...)`, which `THREE.Color.setStyle` understands.\n */\nlet probeCtx: CanvasRenderingContext2D | null | undefined\nfunction getProbeCtx(): CanvasRenderingContext2D | null {\n  if (probeCtx !== undefined) return probeCtx\n  if (typeof document === \"undefined\") return (probeCtx = null)\n  probeCtx = document.createElement(\"canvas\").getContext(\"2d\")\n  return probeCtx\n}\n\n/**\n * Normalise any CSS color string to a form THREE can parse (or `null` if the\n * browser rejects it). Assigning an invalid color to `fillStyle` is a no-op, so\n * we probe with two different sentinels: if the result differs, the input was\n * valid and both reflect its normalised value; if it \"sticks\" to a sentinel,\n * the input was invalid.\n */\nfunction normalizeColor(input: string): string | null {\n  const ctx = getProbeCtx()\n  if (!ctx) return null\n  ctx.fillStyle = \"#000\"\n  ctx.fillStyle = input\n  const a = ctx.fillStyle\n  ctx.fillStyle = \"#fff\"\n  ctx.fillStyle = input\n  const b = ctx.fillStyle\n  return a === b ? a : null\n}\n\n/**\n * Parse a raw CSS color (as read from a custom property) into a `THREE.Color`.\n *\n * shadcn ships colors as bare HSL triplets (\"263 70% 50%\"), which we wrap in\n * `hsl(...)`. Everything else — `oklch()`, `rgb()`, `#hex`, `hsl()`, named — is\n * handed to the browser's parser via a canvas probe, so modern oklch-based\n * themes resolve correctly instead of silently falling back to the default.\n */\nfunction parseColor(raw: string, target: Color): Color {\n  const value = raw.trim()\n  if (!value) return target\n\n  const candidate = HSL_TRIPLET.test(value) ? `hsl(${value})` : value\n\n  // Preferred path: let the browser resolve any CSS color format.\n  const normalized = normalizeColor(candidate)\n  if (normalized) {\n    try {\n      return target.setStyle(normalized)\n    } catch {\n      /* fall through to the manual paths below */\n    }\n  }\n\n  // Fallbacks for environments without a canvas (e.g. exotic SSR shims).\n  const triplet = value.match(/^(-?[\\d.]+)\\s+(-?[\\d.]+)%\\s+(-?[\\d.]+)%/)\n  if (triplet) {\n    const h = parseFloat(triplet[1]) / 360\n    const s = parseFloat(triplet[2]) / 100\n    const l = parseFloat(triplet[3]) / 100\n    return target.setHSL(h, s, l)\n  }\n  try {\n    return target.setStyle(value)\n  } catch {\n    return target\n  }\n}\n\nfunction readColors(previous?: ShadcnThemeColors): ShadcnThemeColors {\n  if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n    return previous ?? createDefaults()\n  }\n\n  const root = document.documentElement\n  const styles = getComputedStyle(root)\n  const base = previous ?? createDefaults()\n\n  const next: ShadcnThemeColors = {\n    ...base,\n    primaryColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.primaryColor),\n      base.primaryColor.clone()\n    ),\n    primaryForegroundColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.primaryForegroundColor),\n      base.primaryForegroundColor.clone()\n    ),\n    bgColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.bgColor),\n      base.bgColor.clone()\n    ),\n    foregroundColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.foregroundColor),\n      base.foregroundColor.clone()\n    ),\n    borderColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.borderColor),\n      base.borderColor.clone()\n    ),\n    mutedColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.mutedColor),\n      base.mutedColor.clone()\n    ),\n    accentColor: parseColor(\n      styles.getPropertyValue(CSS_VARS.accentColor),\n      base.accentColor.clone()\n    ),\n    isDark: root.classList.contains(\"dark\"),\n  }\n\n  return next\n}\n\nexport type ThemeMode = \"auto\" | \"light\" | \"dark\"\n\n/**\n * Bridges your shadcn/ui CSS variables into Three.js.\n *\n * - Reads `--primary`, `--background`, `--border`, ... on mount.\n * - Watches `<html class>` with a MutationObserver and re-reads on dark-mode\n *   toggles (works with next-themes, which flips the `.dark` class).\n * - SSR-safe: returns neutral defaults until the DOM is ready.\n *\n * @param mode `\"auto\"` (default) tracks the document theme. `\"light\"` / `\"dark\"`\n *   force a palette by temporarily toggling a detached probe element.\n */\nexport function useShadcnTheme(mode: ThemeMode = \"auto\"): ShadcnThemeColors {\n  const [colors, setColors] = React.useState<ShadcnThemeColors>(() =>\n    createDefaults()\n  )\n\n  React.useEffect(() => {\n    if (typeof window === \"undefined\") return\n\n    // These reads pull the live values out of an external system (the DOM's\n    // computed CSS variables), which is a legitimate effect → state sync.\n    if (mode !== \"auto\") {\n      // eslint-disable-next-line react-hooks/set-state-in-effect\n      setColors((prev) => readForcedMode(mode, prev))\n      return\n    }\n\n    // Initial read once mounted (handles hydration + system theme).\n    setColors((prev) => readColors(prev))\n\n    const root = document.documentElement\n    const observer = new MutationObserver(() => {\n      setColors((prev) => readColors(prev))\n    })\n    observer.observe(root, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    })\n\n    return () => observer.disconnect()\n  }, [mode])\n\n  return colors\n}\n\n/**\n * Read the palette as it would appear under a forced light/dark mode.\n * We synchronously toggle the `.dark` class on `<html>`, read the computed\n * variables, then restore the original class in the same synchronous block.\n * getComputedStyle forces a style flush, so the values are accurate and the\n * browser never paints the intermediate state (no flicker).\n */\nfunction readForcedMode(\n  mode: \"light\" | \"dark\",\n  previous: ShadcnThemeColors\n): ShadcnThemeColors {\n  if (typeof document === \"undefined\") return previous\n\n  const root = document.documentElement\n  const wasDark = root.classList.contains(\"dark\")\n  const wantDark = mode === \"dark\"\n\n  if (wantDark !== wasDark) root.classList.toggle(\"dark\", wantDark)\n  const result = readColors(previous)\n  if (wantDark !== wasDark) root.classList.toggle(\"dark\", wasDark)\n\n  return result\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:ui"
}