{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "strange-attractor",
  "title": "StrangeAttractor",
  "description": "The Aizawa attractor integrated live into one glowing orbit that never repeats, colored --primary to --accent with a sweeping head.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "files": [
    {
      "path": "components/threecn/strange-attractor.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// Aizawa attractor coefficients — a classic set that traces a spindle wrapped\n// in orbiting loops.\nconst A = 0.95\nconst B = 0.7\nconst C = 0.6\nconst D = 3.5\nconst E = 0.25\nconst F = 0.1\n\n/** Integrate the attractor into a centered, ~4-unit point path (run once). */\nfunction traceAttractor(points: number) {\n  const path = new Float32Array(points * 3)\n  let x = 0.1\n  let y = 0\n  let z = 0\n  const dt = 0.006\n  let minX = Infinity\n  let maxX = -Infinity\n  let minY = Infinity\n  let maxY = -Infinity\n  let minZ = Infinity\n  let maxZ = -Infinity\n\n  for (let i = 0; i < points; i++) {\n    const dx = (z - B) * x - D * y\n    const dy = D * x + (z - B) * y\n    const dz =\n      C + A * z - (z * z * z) / 3 - (x * x + y * y) * (1 + E * z) + F * z * x * x * x\n    x += dx * dt\n    y += dy * dt\n    z += dz * dt\n    path[i * 3] = x\n    path[i * 3 + 1] = y\n    path[i * 3 + 2] = z\n    if (x < minX) minX = x\n    if (x > maxX) maxX = x\n    if (y < minY) minY = y\n    if (y > maxY) maxY = y\n    if (z < minZ) minZ = z\n    if (z > maxZ) maxZ = z\n  }\n\n  const cx = (minX + maxX) / 2\n  const cy = (minY + maxY) / 2\n  const cz = (minZ + maxZ) / 2\n  const span = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1\n  const scale = 4 / span\n  for (let i = 0; i < points; i++) {\n    path[i * 3] = (path[i * 3] - cx) * scale\n    path[i * 3 + 1] = (path[i * 3 + 1] - cy) * scale\n    path[i * 3 + 2] = (path[i * 3 + 2] - cz) * scale\n  }\n  return path\n}\n\n/** Expand the point path into consecutive line segments for <lineSegments>. */\nfunction toSegments(path: Float32Array) {\n  const n = path.length / 3\n  const seg = new Float32Array((n - 1) * 2 * 3)\n  let o = 0\n  for (let i = 0; i < n - 1; i++) {\n    seg[o++] = path[i * 3]\n    seg[o++] = path[i * 3 + 1]\n    seg[o++] = path[i * 3 + 2]\n    seg[o++] = path[(i + 1) * 3]\n    seg[o++] = path[(i + 1) * 3 + 1]\n    seg[o++] = path[(i + 1) * 3 + 2]\n  }\n  return seg\n}\n\nfunction Attractor({\n  points,\n  speed,\n  theme,\n}: {\n  points: number\n  speed: number\n  theme: ThemeMode\n}) {\n  const { primaryColor, accentColor, isDark } = useShadcnTheme(theme)\n  const geoRef = React.useRef<THREE.BufferGeometry>(null)\n  const groupRef = React.useRef<THREE.Group>(null)\n  const headRef = React.useRef<THREE.Mesh>(null)\n  const head = React.useRef(0)\n\n  const path = React.useMemo(() => traceAttractor(points), [points])\n  const segPositions = React.useMemo(() => toSegments(path), [path])\n  const segColors = React.useMemo(\n    () => new Float32Array(segPositions.length),\n    [segPositions]\n  )\n\n  // Gradient --primary → --accent along the orbit; re-run on theme change.\n  React.useLayoutEffect(() => {\n    const attr = geoRef.current?.getAttribute(\"color\") as\n      | THREE.BufferAttribute\n      | undefined\n    if (!attr) return\n    const arr = attr.array as Float32Array\n    const segCount = arr.length / 6\n    const c = new THREE.Color()\n    for (let i = 0; i < segCount; i++) {\n      c.copy(primaryColor).lerp(accentColor, i / segCount)\n      const o = i * 6\n      arr[o] = c.r\n      arr[o + 1] = c.g\n      arr[o + 2] = c.b\n      arr[o + 3] = c.r\n      arr[o + 4] = c.g\n      arr[o + 5] = c.b\n    }\n    attr.needsUpdate = true\n  }, [segPositions, primaryColor, accentColor])\n\n  useFrame((_, delta) => {\n    if (groupRef.current) groupRef.current.rotation.y += delta * 0.25 * speed\n    // A glowing head sweeps continuously along the traced path.\n    const n = path.length / 3\n    head.current = (head.current + delta * 120 * speed) % n\n    const idx = Math.floor(head.current) * 3\n    if (headRef.current) {\n      headRef.current.position.set(path[idx], path[idx + 1], path[idx + 2])\n    }\n  })\n\n  return (\n    <group ref={groupRef} rotation={[0.3, 0, 0.2]}>\n      <lineSegments frustumCulled={false}>\n        <bufferGeometry ref={geoRef}>\n          <bufferAttribute\n            attach=\"attributes-position\"\n            args={[segPositions, 3]}\n          />\n          <bufferAttribute attach=\"attributes-color\" args={[segColors, 3]} />\n        </bufferGeometry>\n        <lineBasicMaterial\n          vertexColors\n          transparent\n          opacity={isDark ? 0.85 : 0.95}\n          depthWrite={false}\n          blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}\n        />\n      </lineSegments>\n      <mesh ref={headRef}>\n        <sphereGeometry args={[0.08, 16, 16]} />\n        <meshBasicMaterial color={accentColor} toneMapped={false} />\n      </mesh>\n    </group>\n  )\n}\n\nexport type StrangeAttractorProps = {\n  /** Number of integration steps traced (path detail). */\n  points?: number\n  /** Rotation and head speed multiplier. */\n  speed?: number\n  className?: string\n  theme?: ThemeMode\n  environment?: SceneContainerProps[\"environment\"]\n}\n\n/**\n * The Aizawa strange attractor, integrated live and drawn as a single glowing\n * orbit that never quite repeats. A bright head sweeps the path while the whole\n * form turns. Colored `--primary` → `--accent`; additive glow in dark mode.\n */\nexport function StrangeAttractor({\n  points = 6000,\n  speed = 1,\n  className,\n  theme = \"auto\",\n  environment = \"night\",\n}: StrangeAttractorProps) {\n  return (\n    <SceneContainer\n      className={className}\n      theme={theme}\n      environment={environment}\n      camera={[0, 0, 7]}\n      fov={45}\n    >\n      <Attractor points={points} speed={speed} 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"
}