{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "presence-field",
  "title": "Presence Field",
  "description": "An ambient backdrop that carries what an assistant is doing, and settles rather than snaps.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/presence-field/presence-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  createQuadProgram,\n  useThemeColors,\n  type QuadProgram,\n  type SurfaceColor,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nexport type AgentPresence = \"idle\" | \"thinking\" | \"streaming\" | \"done\" | \"error\"\n\nexport type PresenceFieldProps = React.HTMLAttributes<HTMLDivElement> & {\n  state?: AgentPresence\n  /** Colour the field settles to. */\n  base?: string\n  /** Colour it takes while it is working. */\n  active?: string\n  /** Colour it takes when something went wrong. */\n  fault?: string\n  /** Colour it rests in. */\n  quiet?: string\n  /** Nought to one, for how much is arriving. Only read while streaming. */\n  activity?: number\n  surfaceClassName?: string\n}\n\ntype Mood = {\n  speed: number\n  strength: number\n  token: \"active\" | \"fault\" | \"quiet\"\n}\n\nconst MOODS: Record<AgentPresence, Mood> = {\n  idle: { speed: 0.18, strength: 0.22, token: \"quiet\" },\n  thinking: { speed: 1, strength: 0.72, token: \"active\" },\n  streaming: { speed: 1.5, strength: 0.9, token: \"active\" },\n  done: { speed: 0.12, strength: 0.34, token: \"active\" },\n  error: { speed: 0.45, strength: 0.6, token: \"fault\" },\n}\n\nconst FRAGMENT = `\nprecision highp float;\nvarying vec2 vUv;\nuniform vec2 uResolution;\nuniform float uTime;\nuniform float uStrength;\nuniform vec3 uBase;\nuniform vec3 uTint;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),\n    mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),\n    u.y\n  );\n}\n\nfloat fbm(vec2 p) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  for (int i = 0; i < 4; i++) {\n    value += amplitude * noise(p);\n    p *= 2.02;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\nvoid main() {\n  float aspect = uResolution.x / max(uResolution.y, 1.0);\n  vec2 p = (vUv - 0.5) * vec2(aspect, 1.0) * 2.6;\n\n  vec2 drift = vec2(sin(uTime * 0.21), cos(uTime * 0.18));\n  float f = fbm(p + drift);\n\n  // Brightest at the edges, so the middle stays quiet enough to read on.\n  float rim = smoothstep(0.15, 0.85, length((vUv - 0.5) * vec2(aspect, 1.0)));\n  float amount = smoothstep(0.3, 0.8, f) * uStrength * (0.35 + rim);\n\n  gl_FragColor = vec4(mix(uBase, uTint, clamp(amount, 0.0, 1.0)), 1.0);\n}\n`\n\ntype Scene = { quad: QuadProgram | null; speed: number; strength: number }\n\n/**\n * An ambient backdrop that carries what the assistant is doing. It is a second\n * channel for something already said in words: put it behind a thread whose\n * thinking state is written out, never in place of one.\n */\nexport function PresenceField({\n  state = \"idle\",\n  base = \"--background\",\n  active = \"--primary\",\n  fault = \"--destructive\",\n  quiet = \"--muted-foreground\",\n  activity = 0.5,\n  className,\n  surfaceClassName,\n  children,\n  ...rootProps\n}: PresenceFieldProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const mood = MOODS[state]\n\n  const tokens = React.useMemo(\n    () =>\n      [base, active, fault, quiet].filter((entry) => entry.startsWith(\"--\")),\n    [active, base, fault, quiet]\n  )\n\n  const resolved = useThemeColors(rootRef, tokens)\n  const pick = { active, fault, quiet }[mood.token]\n  const baseColor = base.startsWith(\"--\") ? resolved[base] : undefined\n  const tintColor = pick.startsWith(\"--\") ? resolved[pick] : undefined\n  const ready = baseColor !== undefined && tintColor !== undefined\n\n  const target = React.useRef({\n    base: [0, 0, 0] as SurfaceColor,\n    tint: [1, 1, 1] as SurfaceColor,\n    speed: mood.speed,\n    strength: mood.strength,\n  })\n\n  React.useEffect(() => {\n    const boost = state === \"streaming\" ? 0.6 + activity * 0.8 : 1\n\n    target.current = {\n      base: baseColor ?? target.current.base,\n      tint: tintColor ?? target.current.tint,\n      speed: mood.speed * boost,\n      strength: mood.strength,\n    }\n  }, [activity, baseColor, mood.speed, mood.strength, state, tintColor])\n\n  // Typed arrays so the easing indexes a number rather than a maybe, and so\n  // the uniforms are uploaded without allocating on every frame.\n  const current = React.useRef({\n    base: new Float32Array([0, 0, 0]),\n    tint: new Float32Array([1, 1, 1]),\n    started: false,\n  })\n\n  const setup = React.useCallback(\n    ({ context }: { context: WebGLRenderingContext }): Scene => ({\n      quad: createQuadProgram(context, FRAGMENT),\n      speed: target.current.speed,\n      strength: target.current.strength,\n    }),\n    []\n  )\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      state: scene,\n      time,\n      delta,\n    }: {\n      context: WebGLRenderingContext\n      size: { width: number; height: number; dpr: number }\n      state: Scene\n      time: number\n      delta: number\n    }) => {\n      const quad = scene.quad\n      if (!quad) return\n\n      // The mood arrives rather than switching, so a thread that finishes\n      // settles instead of snapping to a new colour.\n      const rate = Math.min(1, delta * 2.2)\n      const goal = target.current\n\n      const held = current.current\n\n      if (!held.started) {\n        held.base.set(goal.base)\n        held.tint.set(goal.tint)\n        held.started = true\n      }\n\n      for (let channel = 0; channel < 3; channel += 1) {\n        const currentBase = held.base[channel] ?? 0\n        const currentTint = held.tint[channel] ?? 0\n\n        held.base[channel] =\n          currentBase + ((goal.base[channel] ?? 0) - currentBase) * rate\n        held.tint[channel] =\n          currentTint + ((goal.tint[channel] ?? 0) - currentTint) * rate\n      }\n\n      scene.speed += (goal.speed - scene.speed) * rate\n      scene.strength += (goal.strength - scene.strength) * rate\n\n      const { width, height, dpr } = size\n      context.useProgram(quad.program)\n      context.uniform2f(quad.uniform(\"uResolution\"), width * dpr, height * dpr)\n      context.uniform1f(quad.uniform(\"uTime\"), time * scene.speed)\n      context.uniform1f(quad.uniform(\"uStrength\"), scene.strength)\n      context.uniform3fv(quad.uniform(\"uBase\"), held.base)\n      context.uniform3fv(quad.uniform(\"uTint\"), held.tint)\n\n      quad.paint(width, height, dpr)\n    },\n    []\n  )\n\n  const teardown = React.useCallback((scene: Scene) => {\n    scene.quad?.dispose()\n  }, [])\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"presence-field\"\n      data-state={state}\n      className={cn(\n        \"bg-background relative isolate overflow-hidden\",\n        className\n      )}\n      {...rootProps}\n    >\n      {ready && (\n        <RenderSurface<Scene, \"webgl\">\n          contextType=\"webgl\"\n          setup={setup}\n          draw={draw}\n          teardown={teardown}\n          className={cn(\n            \"pointer-events-none absolute inset-0 -z-10\",\n            surfaceClassName\n          )}\n        />\n      )}\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "agent",
    "scene",
    "status"
  ],
  "type": "registry:ui"
}