{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-persona",
  "title": "AI: Persona",
  "description": "A stateful Rive agent persona animation with application-owned activation and load/error handling.",
  "dependencies": [
    "@rive-app/react-webgl2@^4.36.0"
  ],
  "registryDependencies": [
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/ai/persona.tsx",
      "content": "// Ported from vercel/ai-elements@6a9d5b1822ffb10bba4bd97175f01edd7d8651cd.\n\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { RiveParameters } from \"@rive-app/react-webgl2\";\nimport {\n  useRive,\n  useStateMachineInput,\n  useViewModel,\n  useViewModelInstance,\n  useViewModelInstanceColor,\n} from \"@rive-app/react-webgl2\";\nimport type { FC, ReactNode } from \"react\";\nimport { memo, useEffect, useMemo, useRef, useState } from \"react\";\n\n// Delays Rive initialization by one frame so that React Strict Mode's\n// immediate unmount cycle never creates a WebGL2 context. Only the\n// second (real) mount will initialise, avoiding context exhaustion.\nconst useStrictModeSafeInit = () => {\n  const [ready, setReady] = useState(false);\n\n  useEffect(() => {\n    const id = requestAnimationFrame(() => setReady(true));\n    return () => {\n      cancelAnimationFrame(id);\n      setReady(false);\n    };\n  }, []);\n\n  return ready;\n};\n\nconst useReducedMotion = () => {\n  // Start paused so server and first client render agree, and so WebGL never\n  // autoplays before the browser preference is known.\n  const [reducedMotion, setReducedMotion] = useState(true);\n\n  useEffect(() => {\n    if (!window.matchMedia) {\n      setReducedMotion(false);\n      return;\n    }\n    const preference = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const handleChange = () => setReducedMotion(preference.matches);\n    handleChange();\n    preference.addEventListener(\"change\", handleChange);\n    return () => preference.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  return reducedMotion;\n};\n\nexport type PersonaState =\n  | \"idle\"\n  | \"listening\"\n  | \"thinking\"\n  | \"speaking\"\n  | \"asleep\";\n\ninterface PersonaProps {\n  state: PersonaState;\n  onLoad?: RiveParameters[\"onLoad\"];\n  onLoadError?: RiveParameters[\"onLoadError\"];\n  onReady?: () => void;\n  onPause?: RiveParameters[\"onPause\"];\n  onPlay?: RiveParameters[\"onPlay\"];\n  onStop?: RiveParameters[\"onStop\"];\n  className?: string;\n  variant?: keyof typeof sources;\n}\n\n// The state machine name is always 'default' for Elements AI visuals\nconst stateMachine = \"default\";\n\nconst sources = {\n  command: {\n    dynamicColor: true,\n    hasModel: true,\n    source:\n      \"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/command-2.0.riv\",\n  },\n  glint: {\n    dynamicColor: true,\n    hasModel: true,\n    source:\n      \"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/glint-2.0.riv\",\n  },\n  halo: {\n    dynamicColor: true,\n    hasModel: true,\n    source:\n      \"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/halo-2.0.riv\",\n  },\n  mana: {\n    dynamicColor: false,\n    hasModel: true,\n    source:\n      \"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/mana-2.0.riv\",\n  },\n  obsidian: {\n    dynamicColor: true,\n    hasModel: true,\n    source:\n      \"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/obsidian-2.0.riv\",\n  },\n  opal: {\n    dynamicColor: false,\n    hasModel: false,\n    source:\n      \"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/orb-1.2.riv\",\n  },\n};\n\nconst getCurrentTheme = (): \"light\" | \"dark\" => {\n  if (typeof window !== \"undefined\") {\n    if (document.documentElement.classList.contains(\"dark-mode\")) {\n      return \"dark\";\n    }\n    if (document.documentElement.classList.contains(\"light-mode\")) {\n      return \"light\";\n    }\n    if (window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches) {\n      return \"dark\";\n    }\n  }\n  return \"light\";\n};\n\nconst useTheme = (enabled: boolean) => {\n  const [theme, setTheme] = useState<\"light\" | \"dark\">(getCurrentTheme);\n\n  useEffect(() => {\n    // Skip if not enabled (avoids unnecessary observers for non-dynamic-color variants)\n    if (!enabled) {\n      return;\n    }\n\n    // Watch for classList changes\n    const observer = new MutationObserver(() => {\n      setTheme(getCurrentTheme());\n    });\n\n    observer.observe(document.documentElement, {\n      attributeFilter: [\"class\"],\n      attributes: true,\n    });\n\n    // Watch for OS-level theme changes\n    let mql: MediaQueryList | null = null;\n    const handleMediaChange = () => {\n      setTheme(getCurrentTheme());\n    };\n\n    if (window.matchMedia) {\n      mql = window.matchMedia(\"(prefers-color-scheme: dark)\");\n      mql.addEventListener(\"change\", handleMediaChange);\n    }\n\n    return () => {\n      observer.disconnect();\n      if (mql) {\n        mql.removeEventListener(\"change\", handleMediaChange);\n      }\n    };\n  }, [enabled]);\n\n  return theme;\n};\n\ninterface PersonaWithModelProps {\n  rive: ReturnType<typeof useRive>[\"rive\"];\n  source: (typeof sources)[keyof typeof sources];\n  children: React.ReactNode;\n}\n\nconst PersonaWithModel = memo(\n  ({ rive, source, children }: PersonaWithModelProps) => {\n    const theme = useTheme(source.dynamicColor);\n    const viewModel = useViewModel(rive, { useDefault: true });\n    const viewModelInstance = useViewModelInstance(viewModel, {\n      rive,\n      useDefault: true,\n    });\n    const viewModelInstanceColor = useViewModelInstanceColor(\n      \"color\",\n      viewModelInstance\n    );\n\n    useEffect(() => {\n      if (!(viewModelInstanceColor && source.dynamicColor)) {\n        return;\n      }\n\n      const [r, g, b] = theme === \"dark\" ? [255, 255, 255] : [0, 0, 0];\n      viewModelInstanceColor.setRgb(r, g, b);\n    }, [viewModelInstanceColor, theme, source.dynamicColor]);\n\n    return children;\n  }\n);\n\nPersonaWithModel.displayName = \"PersonaWithModel\";\n\ninterface PersonaWithoutModelProps {\n  children: ReactNode;\n}\n\nconst PersonaWithoutModel = memo(\n  ({ children }: PersonaWithoutModelProps) => children\n);\n\nPersonaWithoutModel.displayName = \"PersonaWithoutModel\";\n\nexport const Persona: FC<PersonaProps> = memo(\n  ({\n    variant = \"obsidian\",\n    state = \"idle\",\n    onLoad,\n    onLoadError,\n    onReady,\n    onPause,\n    onPlay,\n    onStop,\n    className,\n  }) => {\n    const source = sources[variant];\n\n    if (!source) {\n      throw new Error(`Invalid variant: ${variant}`);\n    }\n\n    // Stabilize callbacks to prevent useRive from reinitializing\n    const callbacksRef = useRef({\n      onLoad,\n      onLoadError,\n      onPause,\n      onPlay,\n      onReady,\n      onStop,\n    });\n\n    useEffect(() => {\n      callbacksRef.current = {\n        onLoad,\n        onLoadError,\n        onPause,\n        onPlay,\n        onReady,\n        onStop,\n      };\n    }, [onLoad, onLoadError, onPause, onPlay, onReady, onStop]);\n\n    const stableCallbacks = useMemo(\n      () => ({\n        onLoad: ((loadedRive) =>\n          callbacksRef.current.onLoad?.(\n            loadedRive\n          )) as RiveParameters[\"onLoad\"],\n        onLoadError: ((err) =>\n          callbacksRef.current.onLoadError?.(\n            err\n          )) as RiveParameters[\"onLoadError\"],\n        onPause: ((event) =>\n          callbacksRef.current.onPause?.(event)) as RiveParameters[\"onPause\"],\n        onPlay: ((event) =>\n          callbacksRef.current.onPlay?.(event)) as RiveParameters[\"onPlay\"],\n        onReady: () => callbacksRef.current.onReady?.(),\n        onStop: ((event) =>\n          callbacksRef.current.onStop?.(event)) as RiveParameters[\"onStop\"],\n      }),\n      []\n    );\n\n    // Delay initialisation by one frame to avoid creating (and leaking)\n    // a WebGL2 context during React Strict Mode's first throw-away mount.\n    const reducedMotion = useReducedMotion();\n    const ready = useStrictModeSafeInit();\n\n    const { rive, RiveComponent } = useRive(\n      ready\n        ? {\n            // Start explicitly after preference detection; changing the\n            // preference should pause/play, not recreate the WebGL instance.\n            autoplay: false,\n            onLoad: stableCallbacks.onLoad,\n            onLoadError: stableCallbacks.onLoadError,\n            onPause: stableCallbacks.onPause,\n            onPlay: stableCallbacks.onPlay,\n            onRiveReady: stableCallbacks.onReady,\n            onStop: stableCallbacks.onStop,\n            src: source.source,\n            stateMachines: stateMachine,\n          }\n        : null\n    );\n\n    useEffect(() => {\n      if (!rive) return;\n      if (reducedMotion) rive.pause();\n      else rive.play();\n    }, [reducedMotion, rive]);\n\n    const listeningInput = useStateMachineInput(\n      rive,\n      stateMachine,\n      \"listening\"\n    );\n    const thinkingInput = useStateMachineInput(rive, stateMachine, \"thinking\");\n    const speakingInput = useStateMachineInput(rive, stateMachine, \"speaking\");\n    const asleepInput = useStateMachineInput(rive, stateMachine, \"asleep\");\n\n    // Rive state machine inputs are mutable objects that must be set via direct\n    // property assignment — this is the intended Rive API, not a React anti-pattern.\n    useEffect(() => {\n      if (listeningInput) {\n        listeningInput.value = state === \"listening\";\n      }\n      if (thinkingInput) {\n        thinkingInput.value = state === \"thinking\";\n      }\n      if (speakingInput) {\n        speakingInput.value = state === \"speaking\";\n      }\n      if (asleepInput) {\n        asleepInput.value = state === \"asleep\";\n      }\n    }, [state, listeningInput, thinkingInput, speakingInput, asleepInput]);\n\n    const Component = source.hasModel ? PersonaWithModel : PersonaWithoutModel;\n\n    return (\n      <Component rive={rive} source={source}>\n        <RiveComponent\n          className={cn(\"size-16 shrink-0\", className)}\n          data-reduced-motion={reducedMotion}\n        />\n      </Component>\n    );\n  }\n);\n\nPersona.displayName = \"Persona\";\n",
      "type": "registry:ui",
      "target": "components/ai/persona.tsx"
    }
  ],
  "type": "registry:ui"
}