{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pitch-momentum",
  "title": "PitchMomentum",
  "description": "A soccer pitch of dots where two rival particle mountains surge and clash as momentum swings, crests whitening where the fronts collide.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "files": [
    {
      "path": "components/threecn/pitch-momentum.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useFrame } from \"@react-three/fiber\"\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/** Rounded platform the pitch sits on. */\nconst SLAB_W = 7.8\nconst SLAB_H = 5.2\nconst SLAB_R = 0.55\nconst SLAB_DEPTH = 0.16\n\n/** Playing field (105m × 68m scaled down), markings derive from FIFA metrics. */\nconst PITCH_W = 6.8\nconst PITCH_H = PITCH_W * (68 / 105)\nconst M = PITCH_W / 105 // meters → scene units\nconst PEN_DEPTH = 16.5 * M\nconst PEN_WIDTH = 40.32 * M\nconst GOAL_AREA_DEPTH = 5.5 * M\nconst GOAL_AREA_WIDTH = 18.32 * M\nconst SPOT_DIST = 11 * M\nconst CIRCLE_R = 9.15 * M\nconst GOAL_W = 7.32 * M\nconst GOAL_H = 2.44 * M\nconst GOAL_D = 0.18\n\n/** How far mountains spread and how strongly detail noise crags them. */\nconst SIGMA = 1.6\nconst INV_2S2 = 1 / (2 * SIGMA * SIGMA)\n/** Squashes the across-pitch axis so mountains stretch over the full width. */\nconst Z_WEIGHT = 0.45\n\nconst LINE_Y = 0.012\n\nfunction hash2(ix: number, iz: number) {\n  const s = Math.sin(ix * 127.1 + iz * 311.7) * 43758.5453\n  return s - Math.floor(s)\n}\n\n/** Bilinear value noise in [0, 1]. */\nfunction vnoise(x: number, z: number) {\n  const ix = Math.floor(x)\n  const iz = Math.floor(z)\n  const fx = x - ix\n  const fz = z - iz\n  const ux = fx * fx * (3 - 2 * fx)\n  const uz = fz * fz * (3 - 2 * fz)\n  const a = hash2(ix, iz)\n  const b = hash2(ix + 1, iz)\n  const c = hash2(ix, iz + 1)\n  const d = hash2(ix + 1, iz + 1)\n  return a + (b - a) * ux + (c - a) * uz + (a - b - c + d) * ux * uz\n}\n\n/** Ridged fractal noise — sharp crests, craggy slopes. */\nfunction ridgedFbm(x: number, z: number) {\n  let sum = 0\n  let amp = 0.5\n  let freq = 1\n  let px = x\n  let pz = z\n  for (let o = 0; o < 4; o++) {\n    const n = 1 - Math.abs(2 * vnoise(px * freq, pz * freq) - 1)\n    sum += n * n * amp\n    amp *= 0.5\n    freq *= 2.1\n    // Rotate each octave so the lattice never lines up into visible bands.\n    const rx = px * 0.7648 - pz * 0.6442\n    pz = px * 0.6442 + pz * 0.7648 + 13.7\n    px = rx + 7.3\n  }\n  return sum // ≈ [0, 1]\n}\n\nfunction insideSlab(x: number, z: number) {\n  const dx = Math.abs(x) - (SLAB_W / 2 - SLAB_R)\n  const dz = Math.abs(z) - (SLAB_H / 2 - SLAB_R)\n  if (dx <= 0 || dz <= 0) {\n    return Math.abs(x) <= SLAB_W / 2 && Math.abs(z) <= SLAB_H / 2\n  }\n  return dx * dx + dz * dz <= SLAB_R * SLAB_R\n}\n\n/** All field markings + both goals as one LineSegments position buffer. */\nfunction buildMarkings(): Float32Array {\n  const out: number[] = []\n  const seg = (x1: number, z1: number, x2: number, z2: number) => {\n    out.push(x1, LINE_Y, z1, x2, LINE_Y, z2)\n  }\n  const rect = (cx: number, hw: number, hh: number) => {\n    seg(cx - hw, -hh, cx + hw, -hh)\n    seg(cx + hw, -hh, cx + hw, hh)\n    seg(cx + hw, hh, cx - hw, hh)\n    seg(cx - hw, hh, cx - hw, -hh)\n  }\n  const arc = (\n    cx: number,\n    cz: number,\n    r: number,\n    a0: number,\n    a1: number,\n    n: number\n  ) => {\n    for (let i = 0; i < n; i++) {\n      const t0 = a0 + ((a1 - a0) * i) / n\n      const t1 = a0 + ((a1 - a0) * (i + 1)) / n\n      seg(\n        cx + r * Math.cos(t0),\n        cz + r * Math.sin(t0),\n        cx + r * Math.cos(t1),\n        cz + r * Math.sin(t1)\n      )\n    }\n  }\n\n  // Touchlines, halfway line, center circle and spot.\n  rect(0, PITCH_W / 2, PITCH_H / 2)\n  seg(0, -PITCH_H / 2, 0, PITCH_H / 2)\n  arc(0, 0, CIRCLE_R, 0, Math.PI * 2, 64)\n  arc(0, 0, 0.025, 0, Math.PI * 2, 8)\n\n  for (const side of [-1, 1]) {\n    const goalLine = (side * PITCH_W) / 2\n    // Penalty and goal areas (rectangles opening into the field).\n    const penX = goalLine - (side * PEN_DEPTH) / 2\n    rect(penX, PEN_DEPTH / 2, PEN_WIDTH / 2)\n    const gaX = goalLine - (side * GOAL_AREA_DEPTH) / 2\n    rect(gaX, GOAL_AREA_DEPTH / 2, GOAL_AREA_WIDTH / 2)\n    // Penalty spot + arc (\"the D\") bulging toward midfield.\n    const spotX = goalLine - side * SPOT_DIST\n    arc(spotX, 0, 0.025, 0, Math.PI * 2, 8)\n    const phi = Math.acos((PEN_DEPTH - SPOT_DIST) / CIRCLE_R)\n    const mid = side > 0 ? Math.PI : 0\n    arc(spotX, 0, CIRCLE_R, mid - phi, mid + phi, 20)\n\n    // Goal: a wireframe box sitting behind the goal line.\n    const x0 = goalLine\n    const x1 = goalLine + side * GOAL_D\n    const w = GOAL_W / 2\n    const box = (x: number) => {\n      out.push(x, 0, -w, x, GOAL_H, -w)\n      out.push(x, 0, w, x, GOAL_H, w)\n      out.push(x, GOAL_H, -w, x, GOAL_H, w)\n      out.push(x, 0, -w, x, 0, w)\n    }\n    box(x0)\n    box(x1)\n    for (const zc of [-w, w]) {\n      out.push(x0, 0, zc, x1, 0, zc)\n      out.push(x0, GOAL_H, zc, x1, GOAL_H, zc)\n    }\n  }\n  return Float32Array.from(out)\n}\n\ntype Grid = {\n  positions: Float32Array\n  colors: Float32Array\n  /** Static craggy detail per point. */\n  crag: Float32Array\n  /** Falloff to zero outside the touchlines, so mountains stay on the field. */\n  envelope: Float32Array\n  count: number\n}\n\nfunction buildGrid(density: number): Grid {\n  const nx = density\n  const nz = Math.round((density * SLAB_H) / SLAB_W)\n  const positions: number[] = []\n  const crag: number[] = []\n  const envelope: number[] = []\n  for (let i = 0; i < nx; i++) {\n    for (let j = 0; j < nz; j++) {\n      const x = (i / (nx - 1) - 0.5) * SLAB_W\n      const z = (j / (nz - 1) - 0.5) * SLAB_H\n      if (!insideSlab(x, z)) continue\n      positions.push(x, 0, z)\n      crag.push(ridgedFbm(x * 1.7 + 31.4, z * 1.7 + 17.9))\n      const inX = PITCH_W / 2 - Math.abs(x)\n      const inZ = PITCH_H / 2 - Math.abs(z)\n      const m = Math.min(inX, inZ) / 0.55\n      const c = Math.min(1, Math.max(0, m))\n      envelope.push(c * c * (3 - 2 * c))\n    }\n  }\n  const count = positions.length / 3\n  return {\n    positions: Float32Array.from(positions),\n    colors: new Float32Array(count * 3),\n    crag: Float32Array.from(crag),\n    envelope: Float32Array.from(envelope),\n    count,\n  }\n}\n\nfunction Pitch({\n  speed,\n  height,\n  density,\n  colorA,\n  colorB,\n  theme,\n}: {\n  speed: number\n  height: number\n  density: number\n  colorA: string\n  colorB: string\n  theme: ThemeMode\n}) {\n  const { bgColor, foregroundColor, borderColor, mutedColor, isDark } =\n    useShadcnTheme(theme)\n  const pointsRef = React.useRef<THREE.Points>(null)\n  const tmpColor = React.useMemo(() => new THREE.Color(), [])\n  const teamA = React.useMemo(() => new THREE.Color(colorA), [colorA])\n  const teamB = React.useMemo(() => new THREE.Color(colorB), [colorB])\n  const white = React.useMemo(() => new THREE.Color(\"#ffffff\"), [])\n\n  const grid = React.useMemo(() => buildGrid(density), [density])\n  const markings = React.useMemo(() => buildMarkings(), [])\n\n  const slabGeometry = React.useMemo(() => {\n    const s = new THREE.Shape()\n    const w = SLAB_W / 2\n    const h = SLAB_H / 2\n    const r = SLAB_R\n    s.moveTo(-w + r, -h)\n    s.lineTo(w - r, -h)\n    s.absarc(w - r, -h + r, r, -Math.PI / 2, 0, false)\n    s.lineTo(w, h - r)\n    s.absarc(w - r, h - r, r, 0, Math.PI / 2, false)\n    s.lineTo(-w + r, h)\n    s.absarc(-w + r, h - r, r, Math.PI / 2, Math.PI, false)\n    s.lineTo(-w, -h + r)\n    s.absarc(-w + r, -h + r, r, Math.PI, Math.PI * 1.5, false)\n    const geo = new THREE.ExtrudeGeometry(s, {\n      depth: SLAB_DEPTH,\n      bevelEnabled: false,\n    })\n    geo.rotateX(-Math.PI / 2)\n    geo.translate(0, -0.002, 0)\n    return geo\n  }, [])\n\n  React.useEffect(() => {\n    const current = slabGeometry\n    return () => current.dispose()\n  }, [slabGeometry])\n\n  // The platform stays dark in both modes (a dark stage suits the momentum\n  // mountains); in light mode it reads as a dark card on a light page, so the\n  // lines/dots swap to the light end of the palette for contrast.\n  const slabColor = React.useMemo(\n    () =>\n      isDark\n        ? bgColor.clone().lerp(foregroundColor, 0.06)\n        : foregroundColor.clone().lerp(bgColor, 0.12),\n    [bgColor, foregroundColor, isDark]\n  )\n  const lineColor = isDark ? foregroundColor : bgColor\n  const baseDotColor = isDark ? borderColor : mutedColor\n\n  useFrame(({ clock }) => {\n    const points = pointsRef.current\n    if (!points) return\n    const t = clock.getElapsedTime() * speed\n\n    // Each team's momentum breathes on its own rhythm, and its mountain\n    // wanders around its half — the front line between them keeps shifting.\n    const sA = 0.675 + 0.325 * Math.sin(t * 0.29 + 1.3)\n    const sB = 0.675 + 0.325 * Math.sin(t * 0.37 + 4.1)\n    const cxA = -0.95 + 0.5 * Math.sin(t * 0.13)\n    const czA = 0.5 * Math.sin(t * 0.21 + 2)\n    const cxB = 0.95 + 0.5 * Math.sin(t * 0.17 + 0.7)\n    const czB = 0.5 * Math.sin(t * 0.11 + 5)\n\n    // Write through the live geometry attributes (they wrap grid's buffers).\n    const posAttr = points.geometry.attributes.position as THREE.BufferAttribute\n    const colAttr = points.geometry.attributes.color as THREE.BufferAttribute\n    const positions = posAttr.array as Float32Array\n    const colors = colAttr.array as Float32Array\n    const { crag, envelope, count } = grid\n    for (let i = 0; i < count; i++) {\n      const x = positions[i * 3]\n      const z = positions[i * 3 + 2]\n      const dA = (x - cxA) * (x - cxA) + (z - czA) * (z - czA) * Z_WEIGHT\n      const dB = (x - cxB) * (x - cxB) + (z - czB) * (z - czB) * Z_WEIGHT\n      const gA = sA * Math.exp(-dA * INV_2S2)\n      const gB = sB * Math.exp(-dB * INV_2S2)\n\n      const detail = 0.35 + 0.65 * crag[i]\n      const shimmer = 1 + 0.04 * Math.sin(t * 1.3 + x * 1.9 + z * 2.4)\n      const h = Math.max(gA, gB) * detail * envelope[i] * height * shimmer\n      positions[i * 3 + 1] = h + 0.015\n\n      // Blue where team A dominates, red where team B does; crests whiten.\n      const total = gA + gB\n      const mix = total > 1e-5 ? gB / total : 0.5\n      const k = Math.min(1, Math.max(0, (mix - 0.5) * 3 + 0.5))\n      tmpColor.copy(teamA).lerp(teamB, k)\n      const lift = Math.min(1, h / (Math.max(height, 1e-5) * 0.75))\n      tmpColor.lerpColors(\n        baseDotColor,\n        tmpColor,\n        Math.min(1, 0.15 + 0.85 * Math.min(1, lift * 1.4))\n      )\n      const crest = Math.max(0, lift - 0.55) / 0.45\n      tmpColor.lerp(white, Math.min(0.85, crest * crest * 0.85))\n      colors[i * 3] = tmpColor.r\n      colors[i * 3 + 1] = tmpColor.g\n      colors[i * 3 + 2] = tmpColor.b\n    }\n    posAttr.needsUpdate = true\n    colAttr.needsUpdate = true\n  })\n\n  return (\n    <group>\n      <mesh geometry={slabGeometry} position={[0, -SLAB_DEPTH, 0]}>\n        <meshStandardMaterial\n          color={slabColor}\n          roughness={0.85}\n          metalness={0.1}\n        />\n      </mesh>\n      <lineSegments>\n        <bufferGeometry>\n          <bufferAttribute attach=\"attributes-position\" args={[markings, 3]} />\n        </bufferGeometry>\n        <lineBasicMaterial color={lineColor} transparent opacity={0.7} />\n      </lineSegments>\n      <points key={density} ref={pointsRef} frustumCulled={false}>\n        <bufferGeometry>\n          <bufferAttribute\n            attach=\"attributes-position\"\n            args={[grid.positions, 3]}\n          />\n          <bufferAttribute attach=\"attributes-color\" args={[grid.colors, 3]} />\n        </bufferGeometry>\n        <pointsMaterial size={0.02} vertexColors sizeAttenuation />\n      </points>\n    </group>\n  )\n}\n\nexport type PitchMomentumProps = {\n  /** Animation speed multiplier. */\n  speed?: number\n  /** Peak height of the momentum mountains. */\n  height?: number\n  /** Points across the platform's length (grid resolution). */\n  density?: number\n  /** Color of the first team's mountain. */\n  colorA?: string\n  /** Color of the second team's mountain. */\n  colorB?: string\n  className?: string\n  theme?: ThemeMode\n  environment?: SceneContainerProps[\"environment\"]\n}\n\n/**\n * A soccer pitch rendered as a dense field of dots, where two rival particle\n * mountains — one per team — surge, wander and clash as momentum swings.\n * Crests whiten where the fronts collide; the field markings and platform\n * follow your theme tokens.\n */\nexport function PitchMomentum({\n  speed = 1,\n  height = 1.7,\n  density = 230,\n  colorA = \"#60a5fa\",\n  colorB = \"#dc2626\",\n  className,\n  theme = \"auto\",\n  environment = \"night\",\n}: PitchMomentumProps) {\n  return (\n    <SceneContainer\n      className={className}\n      theme={theme}\n      environment={environment}\n      camera={[0, 3.9, 6]}\n      fov={40}\n    >\n      <Pitch\n        speed={speed}\n        height={height}\n        density={density}\n        colorA={colorA}\n        colorB={colorB}\n        theme={theme}\n      />\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"
}