{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "forecast-cone",
  "type": "registry:component",
  "title": "ForecastCone",
  "description": "A 3D probability cone widening from a 'now' apex into the future, with seeded branching paths inside a translucent uncertainty hull and a sweeping present-ring. Deterministic, no backend — built to show forecasts with uncertainty.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/postprocessing"
  ],
  "registryDependencies": [
    "https://prism.icglabs.co/r/canvas-perf.json"
  ],
  "tier": "free",
  "note": "Render via next/dynamic ssr:false. Self-contained; `spread` controls how uncertain the future is.",
  "files": [
    {
      "path": "components/prism/ForecastCone.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useRef } from \"react\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { EffectComposer, Bloom } from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport { useDeviceTier } from \"@/lib/useDeviceTier\";\nimport { AutoDpr, PauseWhenOffscreen } from \"@/components/showcase/CanvasPerf\";\n\n/**\n * ForecastCone — a 3D probability cone widening from a single \"now\" apex into the\n * future. Seeded Brownian sample paths branch inside a translucent uncertainty\n * hull; a cross-section ring sweeps forward = \"the present moment advancing.\"\n * Deterministic (same `seed` → same cone), so it needs no backend or token.\n *\n * Built for Sky's forecast-with-uncertainty + grounded-vs-imagined framing:\n * `spread` is how uncertain the future is.\n */\nexport type ForecastConeProps = {\n  seed?: string;\n  paths?: number;\n  spread?: number;\n  /** [median, hull, wildcard] hex. */\n  colors?: [string, string, string];\n  className?: string;\n};\n\nfunction mulberry32(seedStr: string) {\n  let h = 1779033703 ^ seedStr.length;\n  for (let i = 0; i < seedStr.length; i++) {\n    h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353);\n    h = (h << 13) | (h >>> 19);\n  }\n  let a = h >>> 0;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst STEPS = 48;\nconst SPAN = 6;\n\nfunction ConeScene({\n  seed,\n  paths,\n  spread,\n  colors,\n  reduced,\n}: {\n  seed: string;\n  paths: number;\n  spread: number;\n  colors: [string, string, string];\n  reduced: boolean;\n}) {\n  const sweep = useRef<THREE.Mesh>(null);\n  const group = useRef<THREE.Group>(null);\n\n  const hullGeo = useMemo(() => {\n    const pts: THREE.Vector2[] = [];\n    for (let i = 0; i <= STEPS; i++) {\n      const t = i / STEPS;\n      pts.push(new THREE.Vector2(spread * Math.sqrt(t) * 1.6, t * SPAN));\n    }\n    const g = new THREE.LatheGeometry(pts, 48);\n    g.rotateZ(-Math.PI / 2); // grow along +x (left = now, right = future)\n    return g;\n  }, [spread]);\n\n  const lines = useMemo(() => {\n    const rnd = mulberry32(seed);\n    const out: { geo: THREE.BufferGeometry; mid: boolean }[] = [];\n    for (let p = 0; p < paths; p++) {\n      const pos = new Float32Array((STEPS + 1) * 3);\n      let y = 0;\n      let z = 0;\n      const mid = p === 0;\n      for (let i = 0; i <= STEPS; i++) {\n        const t = i / STEPS;\n        const cap = spread * Math.sqrt(t) * 1.5;\n        if (!mid) {\n          y += (rnd() - 0.5) * spread * 0.18;\n          z += (rnd() - 0.5) * spread * 0.18;\n          y = Math.max(-cap, Math.min(cap, y));\n          z = Math.max(-cap, Math.min(cap, z));\n        }\n        pos[i * 3] = t * SPAN;\n        pos[i * 3 + 1] = y;\n        pos[i * 3 + 2] = z;\n      }\n      const geo = new THREE.BufferGeometry();\n      geo.setAttribute(\"position\", new THREE.BufferAttribute(pos, 3));\n      out.push({ geo, mid });\n    }\n    return out;\n  }, [seed, paths, spread]);\n\n  const median = useMemo(() => new THREE.Color(colors[0]), [colors]);\n  const hull = useMemo(() => new THREE.Color(colors[1]), [colors]);\n  const wild = useMemo(() => new THREE.Color(colors[2]), [colors]);\n\n  const lineObjs = useMemo(\n    () =>\n      lines.map((l) => {\n        const mat = new THREE.LineBasicMaterial({\n          color: l.mid ? median : wild,\n          transparent: true,\n          opacity: l.mid ? 1 : 0.28,\n          blending: THREE.AdditiveBlending,\n          depthWrite: false,\n        });\n        return new THREE.Line(l.geo, mat);\n      }),\n    [lines, median, wild]\n  );\n\n  useFrame((state) => {\n    if (group.current && !reduced)\n      group.current.rotation.x = Math.sin(state.clock.elapsedTime * 0.15) * 0.12;\n    if (sweep.current) {\n      const tt = reduced ? 0.5 : (state.clock.elapsedTime * 0.25) % 1;\n      sweep.current.position.x = tt * SPAN;\n      const r = Math.max(0.001, spread * Math.sqrt(tt) * 1.5);\n      sweep.current.scale.set(r, r, r);\n    }\n  });\n\n  return (\n    <group ref={group}>\n      <mesh geometry={hullGeo}>\n        <meshBasicMaterial\n          color={hull}\n          transparent\n          opacity={0.12}\n          side={THREE.DoubleSide}\n          depthWrite={false}\n          blending={THREE.AdditiveBlending}\n        />\n      </mesh>\n      <mesh position={[0, 0, 0]}>\n        <sphereGeometry args={[0.05, 16, 16]} />\n        <meshBasicMaterial color={median} />\n      </mesh>\n      {lineObjs.map((o, i) => (\n        <primitive key={i} object={o} />\n      ))}\n      <mesh ref={sweep}>\n        <torusGeometry args={[1, 0.012, 8, 48]} />\n        <meshBasicMaterial color={median} transparent opacity={0.9} blending={THREE.AdditiveBlending} depthWrite={false} />\n      </mesh>\n    </group>\n  );\n}\n\nexport function ForecastCone({\n  seed = \"prism\",\n  paths = 24,\n  spread = 1.4,\n  colors = [\"#1fd4e6\", \"#5b7cff\", \"#ff3d81\"],\n  className,\n}: ForecastConeProps) {\n  const reduced =\n    typeof window !== \"undefined\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  const q = useDeviceTier();\n  const n = Math.max(8, Math.round(paths * q.scale));\n\n  return (\n    <div className={className} style={{ width: \"100%\", height: \"100%\" }}>\n      <Canvas\n        dpr={q.dpr}\n        gl={{ antialias: true, powerPreference: \"high-performance\" }}\n        camera={{ position: [-1.5, 1.4, 5.2], fov: 50 }}\n      >\n        <ConeScene seed={seed} paths={n} spread={spread} colors={colors} reduced={reduced} />\n        <AutoDpr />\n        <PauseWhenOffscreen />\n        {q.bloom && (\n          <EffectComposer>\n            <Bloom intensity={0.9} luminanceThreshold={0} luminanceSmoothing={0.3} mipmapBlur radius={0.6} />\n          </EffectComposer>\n        )}\n      </Canvas>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/prism/ForecastCone.tsx"
    }
  ]
}