{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voronoi-shatter",
  "title": "VoronoiShatter",
  "description": "A sphere fractured into Voronoi shards that drift apart and snap back together, each facet shading --accent to --primary by height.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "files": [
    {
      "path": "components/threecn/voronoi-shatter.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\nconst RADIUS = 1.6\nconst THICKNESS = 0.34\n\ntype Vec = [number, number, number]\n\ntype Shard = {\n  geometry: THREE.BufferGeometry\n  centroid: THREE.Vector3\n  axis: THREE.Vector3\n  spin: number\n}\n\n/** Evenly spread N points on a sphere (Fibonacci lattice) as fracture sites. */\nfunction fibonacciSites(n: number) {\n  const sites: THREE.Vector3[] = []\n  const golden = Math.PI * (3 - Math.sqrt(5))\n  for (let i = 0; i < n; i++) {\n    const y = n === 1 ? 0 : 1 - (i / (n - 1)) * 2\n    const r = Math.sqrt(Math.max(0, 1 - y * y))\n    const t = golden * i\n    sites.push(new THREE.Vector3(Math.cos(t) * r, y, Math.sin(t) * r))\n  }\n  return sites\n}\n\nfunction pushTri(out: number[], a: Vec, b: Vec, c: Vec) {\n  out.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2])\n}\n\n/**\n * Break an icosphere into Voronoi shards (faces assigned to their nearest\n * site), then give every shard real thickness by extruding it toward the\n * center — so each fragment reads as a solid chunk, not a paper-thin shell.\n */\nfunction buildShards(count: number): Shard[] {\n  const base = new THREE.IcosahedronGeometry(RADIUS, 3)\n  const nonIndexed = base.toNonIndexed()\n  base.dispose()\n  const arr = (nonIndexed.getAttribute(\"position\") as THREE.BufferAttribute)\n    .array as Float32Array\n  const sites = fibonacciSites(count)\n  const buckets: number[][] = Array.from({ length: count }, () => [])\n  const f = (RADIUS - THICKNESS) / RADIUS\n  const c = new THREE.Vector3()\n\n  for (let i = 0; i < arr.length; i += 9) {\n    // Nearest fracture site for this face (by its normalized centroid).\n    c.set(\n      (arr[i] + arr[i + 3] + arr[i + 6]) / 3,\n      (arr[i + 1] + arr[i + 4] + arr[i + 7]) / 3,\n      (arr[i + 2] + arr[i + 5] + arr[i + 8]) / 3\n    ).normalize()\n    let best = 0\n    let bestDot = -Infinity\n    for (let s = 0; s < sites.length; s++) {\n      const d = c.dot(sites[s])\n      if (d > bestDot) {\n        bestDot = d\n        best = s\n      }\n    }\n    const out = buckets[best]\n\n    // Outer triangle (on the sphere) and its inward-extruded copy.\n    const p: Vec[] = [\n      [arr[i], arr[i + 1], arr[i + 2]],\n      [arr[i + 3], arr[i + 4], arr[i + 5]],\n      [arr[i + 6], arr[i + 7], arr[i + 8]],\n    ]\n    const q: Vec[] = p.map((v) => [v[0] * f, v[1] * f, v[2] * f])\n\n    pushTri(out, p[0], p[1], p[2]) // outer face\n    pushTri(out, q[0], q[2], q[1]) // inner face (reversed winding)\n    // Side walls around the three edges.\n    const edges: [number, number][] = [\n      [0, 1],\n      [1, 2],\n      [2, 0],\n    ]\n    for (const [a, b] of edges) {\n      pushTri(out, p[a], p[b], q[b])\n      pushTri(out, p[a], q[b], q[a])\n    }\n  }\n  nonIndexed.dispose()\n\n  const shards: Shard[] = []\n  for (const verts of buckets) {\n    if (verts.length === 0) continue\n    const data = Float32Array.from(verts)\n    const centroid = new THREE.Vector3()\n    for (let i = 0; i < data.length; i += 3) {\n      centroid.x += data[i]\n      centroid.y += data[i + 1]\n      centroid.z += data[i + 2]\n    }\n    centroid.multiplyScalar(3 / data.length)\n    // Recenter geometry on its centroid so the mesh can be positioned and\n    // rotated about its own middle.\n    for (let i = 0; i < data.length; i += 3) {\n      data[i] -= centroid.x\n      data[i + 1] -= centroid.y\n      data[i + 2] -= centroid.z\n    }\n    const geometry = new THREE.BufferGeometry()\n    geometry.setAttribute(\"position\", new THREE.BufferAttribute(data, 3))\n    geometry.computeVertexNormals()\n    shards.push({\n      geometry,\n      centroid,\n      axis: new THREE.Vector3(\n        Math.random() - 0.5,\n        Math.random() - 0.5,\n        Math.random() - 0.5\n      ).normalize(),\n      spin: 0.5 + Math.random() * 1.1,\n    })\n  }\n  return shards\n}\n\nfunction Shatter({\n  shards: shardCount,\n  speed,\n  spread,\n  theme,\n}: {\n  shards: number\n  speed: number\n  spread: number\n  theme: ThemeMode\n}) {\n  const { primaryColor, accentColor } = useShadcnTheme(theme)\n  const groupRef = React.useRef<THREE.Group>(null)\n\n  const shards = React.useMemo(() => buildShards(shardCount), [shardCount])\n\n  React.useEffect(() => {\n    // Free GPU geometry when the shard set is rebuilt or unmounted.\n    const current = shards\n    return () => current.forEach((s) => s.geometry.dispose())\n  }, [shards])\n\n  useFrame(({ clock }) => {\n    const group = groupRef.current\n    if (!group) return\n    const t = clock.getElapsedTime() * speed\n    // Breathe from fully assembled (0) to exploded (1) and back. The easing\n    // lingers on the assembled sphere so the shatter reads clearly.\n    const e = (Math.sin(t * 0.8) * 0.5 + 0.5) ** 1.6\n    group.rotation.y = t * 0.12\n\n    for (let i = 0; i < shards.length; i++) {\n      const child = group.children[i] as THREE.Mesh | undefined\n      if (!child) continue\n      const s = shards[i]\n      const grow = 1 + e * spread\n      child.position.set(\n        s.centroid.x * grow,\n        s.centroid.y * grow,\n        s.centroid.z * grow\n      )\n      // Absolute (not accumulated) rotation → identity when assembled.\n      child.quaternion.setFromAxisAngle(s.axis, e * s.spin)\n      const mat = child.material as THREE.MeshStandardMaterial\n      mat.color\n        .copy(accentColor)\n        .lerp(primaryColor, s.centroid.y / (RADIUS * 2) + 0.5)\n    }\n  })\n\n  return (\n    <group ref={groupRef}>\n      {shards.map((s, i) => (\n        <mesh\n          key={i}\n          geometry={s.geometry}\n          position={[s.centroid.x, s.centroid.y, s.centroid.z]}\n        >\n          <meshStandardMaterial\n            roughness={0.32}\n            metalness={0.3}\n            side={THREE.DoubleSide}\n            flatShading\n          />\n        </mesh>\n      ))}\n    </group>\n  )\n}\n\nexport type VoronoiShatterProps = {\n  /** Number of Voronoi fragments. */\n  shards?: number\n  /** Explode/reassemble speed multiplier. */\n  speed?: number\n  /** How far the shards fly apart. */\n  spread?: number\n  className?: string\n  theme?: ThemeMode\n  environment?: SceneContainerProps[\"environment\"]\n}\n\n/**\n * A solid sphere fractured into Voronoi shards that drift apart and snap back\n * together in a slow breath. Each fragment has real thickness, tumbles in\n * place, and shades from `--accent` to `--primary` by height.\n */\nexport function VoronoiShatter({\n  shards = 24,\n  speed = 1,\n  spread = 0.7,\n  className,\n  theme = \"auto\",\n  environment = \"studio\",\n}: VoronoiShatterProps) {\n  return (\n    <SceneContainer\n      className={className}\n      theme={theme}\n      environment={environment}\n      camera={[0, 0, 6]}\n      fov={42}\n    >\n      <Shatter shards={shards} speed={speed} spread={spread} 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"
}