{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "example-voice-app",
  "type": "registry:block",
  "title": "Example Voice App",
  "description": "Talk and type in the same conversation, with one composer and shared context.",
  "dependencies": [
    "@radix-ui/react-avatar@^1.1.3",
    "@radix-ui/react-slot@^1.2.2",
    "@radix-ui/react-tooltip@^1.2.6",
    "class-variance-authority@^0.7.1",
    "clsx@^2.1.1",
    "livekit-client@2.22.3",
    "lucide-react@0.577.0",
    "marked@^15.0.7",
    "motion@12.26.2",
    "next-themes@^0.4.4",
    "radix-ui@1.6.7",
    "react-aria-components@^1.21.1",
    "react-markdown@^10.1.0",
    "remark-breaks@^4.0.0",
    "remark-gfm@^4.0.1",
    "shiki@^3.0.0",
    "tailwind-merge@^3.0.1",
    "tailwindcss@^4.1.7",
    "tw-animate-css@1.3.6",
    "use-stick-to-bottom@^1.1.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/examples/voice-app.tsx",
      "target": "@components/examples/voice-app.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { ConversationAppExample } from \"./conversation-app\"\n\nexport function VoiceAppExample() {\n  return (\n    <ConversationAppExample\n      initialVoice\n      className=\"h-[760px] overflow-hidden rounded-[14px] border\"\n    />\n  )\n}\n"
    },
    {
      "path": "components/examples/conversation-app.tsx",
      "target": "@components/examples/conversation-app.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport {\n  ChatContainerContent,\n  ChatContainerRoot,\n} from \"@/components/prompt-kit/chat-container\"\nimport { Loader } from \"@/components/prompt-kit/loader\"\nimport { Message, MessageContent } from \"@/components/prompt-kit/message\"\nimport {\n  PromptInput,\n  PromptInputAction,\n  PromptInputActions,\n  PromptInputTextarea,\n} from \"@/components/prompt-kit/prompt-input\"\nimport { PromptSuggestion } from \"@/components/prompt-kit/prompt-suggestion\"\nimport { Button } from \"@/components/ui/button\"\nimport { ThemeToggle } from \"@/components/ui/theme-toggle\"\nimport { AgentTrackToggle } from \"@/components/voice-agents/livekit/agent-track-toggle\"\nimport { Shdr21 } from \"@/components/voice-agents/orbkit/shdr-21\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  ArrowUp,\n  AudioLines,\n  FileText,\n  Paperclip,\n  Square,\n  X,\n} from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { useEffect, useRef, useState } from \"react\"\nimport {\n  researchAnswer,\n  researchProjects,\n  type ResearchNote,\n} from \"./research-workflow\"\n\ntype Turn = {\n  id: number\n  role: \"user\" | \"assistant\"\n  text: string\n  input: \"text\" | \"voice\"\n}\nconst starters = [\n  \"Summarize the dock interviews\",\n  \"What should the team change first?\",\n]\nconst sampleSpokenQuestion = \"What is slowing down the dock handoff?\"\n\n/** Composition of kit components with shared text and voice demo state. */\nexport function ConversationAppExample({\n  initialVoice = false,\n  className,\n}: {\n  initialVoice?: boolean\n  className?: string\n} = {}) {\n  const [voice, setVoice] = useState(initialVoice)\n  const [phase, setPhase] = useState<\"idle\" | \"thinking\" | \"speaking\">(\"idle\")\n  const [draft, setDraft] = useState(\"\")\n  const [turns, setTurns] = useState<Turn[]>([])\n  const [muted, setMuted] = useState(false)\n  const [concise, setConcise] = useState(true)\n  const [attachment, setAttachment] = useState<ResearchNote | null>(null)\n  const [notice, setNotice] = useState(\"\")\n  const sequence = useRef(0)\n  const attachmentVersion = useRef(0)\n  const replyTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const fileInput = useRef<HTMLInputElement>(null)\n  const composer = useRef<HTMLDivElement>(null)\n  const reduce = useReducedMotion()\n  const centered = !voice && turns.length === 0\n  const thinking = phase === \"thinking\"\n\n  const clearTimers = () => {\n    if (replyTimer.current) clearTimeout(replyTimer.current)\n    if (settleTimer.current) clearTimeout(settleTimer.current)\n    replyTimer.current = null\n    settleTimer.current = null\n  }\n  useEffect(() => clearTimers, [])\n\n  const send = (text: string, input: Turn[\"input\"] = \"text\") => {\n    if (!text.trim() || thinking || (input === \"voice\" && muted)) return\n    clearTimers()\n    const answer = researchAnswer(\n      text,\n      attachment ? [attachment] : researchProjects[0].notes,\n      concise\n    )\n    const turn: Turn = {\n      id: ++sequence.current,\n      role: \"user\",\n      text: text.trim(),\n      input,\n    }\n    setTurns((current) => [...current, turn])\n    if (input === \"text\") setDraft(\"\")\n    setNotice(\"\")\n    setPhase(\"thinking\")\n    replyTimer.current = setTimeout(() => {\n      const reply: Turn = {\n        id: ++sequence.current,\n        role: \"assistant\",\n        text: answer,\n        input,\n      }\n      setTurns((current) => [...current, reply])\n      setPhase(\"speaking\")\n      replyTimer.current = null\n      settleTimer.current = setTimeout(() => {\n        setPhase(\"idle\")\n        settleTimer.current = null\n      }, 1800)\n    }, 900)\n  }\n  const toggleVoice = () => {\n    setVoice((current) => !current)\n    setNotice(\"\")\n    requestAnimationFrame(() =>\n      composer.current?.querySelector(\"textarea\")?.focus()\n    )\n  }\n  const stop = () => {\n    clearTimers()\n    setPhase(\"idle\")\n    setNotice(\"Response stopped. You can continue the conversation.\")\n  }\n  const reset = () => {\n    clearTimers()\n    setTurns([])\n    setDraft(\"\")\n    setVoice(initialVoice)\n    setPhase(\"idle\")\n    setMuted(false)\n    setConcise(true)\n    attachmentVersion.current += 1\n    setAttachment(null)\n    setNotice(\"\")\n  }\n  const attach = async (file?: File) => {\n    if (!file) return\n    const version = ++attachmentVersion.current\n    if (!/\\.(txt|md|csv)$/i.test(file.name) || file.size > 64 * 1024) {\n      setNotice(\"Choose a text, Markdown, or CSV note under 64 KB.\")\n      return\n    }\n    try {\n      const text = await file.text()\n      if (version !== attachmentVersion.current) return\n      setAttachment({ id: file.name, title: file.name, text })\n      setNotice(\"\")\n    } catch {\n      if (version === attachmentVersion.current)\n        setNotice(\"That note could not be read. Try another file.\")\n    }\n  }\n  const stateLabel = thinking\n    ? \"Thinking\"\n    : phase === \"speaking\"\n      ? \"Speaking\"\n      : muted\n        ? \"Microphone muted\"\n        : \"Listening\"\n\n  return (\n    <section\n      aria-label=\"Fieldwork conversation\"\n      data-conversation-preview\n      data-app-example={initialVoice ? \"voice\" : \"chat\"}\n      className={cn(\n        \"bg-background text-foreground flex h-dvh min-h-[560px] flex-col\",\n        className\n      )}\n    >\n      <header className=\"border-border flex h-16 shrink-0 items-center justify-between gap-3 border-b px-4 sm:px-8\">\n        <div className=\"min-w-0\">\n          <p className=\"text-sm font-medium\">Fieldwork assistant</p>\n          <p className=\"text-muted-foreground truncate text-xs\">\n            Agents Kit · Conversation example\n          </p>\n        </div>\n        <div className=\"flex shrink-0 items-center gap-2\">\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={reset}\n            className=\"text-muted-foreground text-xs\"\n          >\n            Reset preview\n          </Button>\n          <ThemeToggle />\n        </div>\n      </header>\n\n      <div className=\"relative mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col px-4 sm:px-6\">\n        <ChatContainerRoot data-transcript className=\"min-h-0 flex-1\">\n          <ChatContainerContent className=\"gap-6 px-1 pt-8 pb-8 sm:px-2\">\n            {turns.map((turn) => (\n              <Message\n                key={turn.id}\n                data-turn={turn.id}\n                data-input={turn.input}\n                className={cn(turn.role === \"user\" && \"justify-end\")}\n              >\n                <div\n                  className={cn(\n                    \"min-w-0\",\n                    turn.role === \"user\" ? \"max-w-[90%]\" : \"w-full\"\n                  )}\n                >\n                  <p className=\"text-muted-foreground mb-1.5 flex items-center gap-1.5 text-xs\">\n                    {turn.role === \"user\" ? \"You\" : \"Fieldwork assistant\"}\n                    {turn.input === \"voice\" && turn.role === \"user\" && (\n                      <>\n                        <AudioLines className=\"size-3\" aria-hidden />\n                        Voice\n                      </>\n                    )}\n                  </p>\n                  <MessageContent\n                    className={cn(\n                      \"max-w-none text-sm leading-6 whitespace-pre-wrap\",\n                      turn.role === \"assistant\"\n                        ? \"bg-transparent p-0\"\n                        : \"px-3 py-2.5\"\n                    )}\n                  >\n                    {turn.text}\n                  </MessageContent>\n                </div>\n              </Message>\n            ))}\n            {thinking && (\n              <div\n                role=\"status\"\n                className=\"text-muted-foreground flex items-center gap-2 text-xs\"\n              >\n                <Loader variant=\"typing\" size=\"sm\" />\n                Reading the notes…\n              </div>\n            )}\n          </ChatContainerContent>\n        </ChatContainerRoot>\n\n        <motion.div\n          ref={composer}\n          layout=\"position\"\n          data-composer-position\n          transition={{ duration: reduce ? 0 : 0.3, ease: [0.2, 0, 0, 1] }}\n          className={cn(\n            \"relative w-full shrink-0 pb-4\",\n            centered && \"absolute inset-x-0 bottom-[38%] px-4 sm:px-6\"\n          )}\n        >\n          {centered && (\n            <div className=\"mb-6\">\n              <h1 className=\"text-2xl leading-8 font-normal tracking-tight\">\n                Work with your field notes\n              </h1>\n              <p className=\"text-muted-foreground mt-2 text-sm\">\n                Ask a question, talk it through, or add a source.\n              </p>\n            </div>\n          )}\n          <AnimatePresence initial={false}>\n            {voice && (\n              <motion.div\n                key=\"voice\"\n                initial={{ opacity: 0, y: reduce ? 0 : 6 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: reduce ? 0 : 0.15 }}\n                className=\"mb-4 flex items-center gap-4 px-1\"\n              >\n                <div data-voice-orb className=\"size-20 shrink-0\">\n                  <Shdr21\n                    size={80}\n                    state={phase}\n                    maxDpr={2}\n                    pauseOffscreen\n                    ariaLabel={`${stateLabel} voice orb`}\n                  />\n                </div>\n                <div className=\"min-w-0\">\n                  <p role=\"status\" className=\"text-sm font-medium\">\n                    {stateLabel}\n                  </p>\n                  <p className=\"text-muted-foreground mt-1 text-xs leading-5\">\n                    Voice and typing share this conversation.\n                  </p>\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n\n          <PromptInput\n            data-composer\n            value={draft}\n            onValueChange={setDraft}\n            onSubmit={() => send(draft)}\n            isLoading={thinking}\n            maxHeight={144}\n            className=\"bg-card focus-within:ring-ring rounded-xl p-2 focus-within:ring-1\"\n          >\n            {attachment && (\n              <div className=\"bg-secondary mb-2 flex w-fit max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs\">\n                <FileText className=\"size-3.5 shrink-0\" aria-hidden />\n                <span className=\"truncate\">{attachment.title}</span>\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  className=\"size-6\"\n                  aria-label=\"Remove context note\"\n                  onClick={() => {\n                    attachmentVersion.current += 1\n                    setAttachment(null)\n                  }}\n                >\n                  <X className=\"size-3\" />\n                </Button>\n              </div>\n            )}\n            <PromptInputTextarea\n              data-conversation-input\n              aria-label=\"Message\"\n              placeholder={\n                voice\n                  ? \"Keep typing while voice is on…\"\n                  : \"Ask about your field notes…\"\n              }\n              className=\"min-h-12 px-2 py-2 text-base sm:text-sm\"\n            />\n            <PromptInputActions className=\"justify-between gap-2 pt-1\">\n              <div className=\"flex min-w-0 items-center gap-1\">\n                <PromptInputAction tooltip=\"Add a text, Markdown, or CSV note\">\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    aria-label=\"Add context note\"\n                    onClick={() => fileInput.current?.click()}\n                  >\n                    <Paperclip />\n                  </Button>\n                </PromptInputAction>\n                <PromptInputAction\n                  tooltip={\n                    concise ? \"Use a detailed answer\" : \"Use a concise answer\"\n                  }\n                >\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    aria-label=\"Concise answers\"\n                    aria-pressed={concise}\n                    onClick={() => setConcise((value) => !value)}\n                    className=\"text-muted-foreground text-xs\"\n                  >\n                    {concise ? \"Concise\" : \"Detailed\"}\n                  </Button>\n                </PromptInputAction>\n              </div>\n              <div className=\"flex shrink-0 items-center gap-2\">\n                {voice && (\n                  <PromptInputAction\n                    tooltip={muted ? \"Unmute microphone\" : \"Mute microphone\"}\n                  >\n                    <AgentTrackToggle\n                      source=\"microphone\"\n                      pressed={!muted}\n                      onPressedChange={(enabled) => setMuted(!enabled)}\n                      aria-label={\n                        muted ? \"Unmute microphone\" : \"Mute microphone\"\n                      }\n                    />\n                  </PromptInputAction>\n                )}\n                <PromptInputAction\n                  tooltip={voice ? \"Return to typing\" : \"Start voice mode\"}\n                >\n                  <Button\n                    type=\"button\"\n                    variant={voice ? \"secondary\" : \"outline\"}\n                    size=\"icon\"\n                    aria-label={voice ? \"Exit voice mode\" : \"Enter voice mode\"}\n                    onClick={toggleVoice}\n                  >\n                    {voice ? <X /> : <AudioLines />}\n                  </Button>\n                </PromptInputAction>\n                <PromptInputAction\n                  tooltip={thinking ? \"Stop response\" : \"Send message\"}\n                >\n                  <Button\n                    type=\"button\"\n                    size=\"icon\"\n                    disabled={!thinking && !draft.trim()}\n                    aria-label={thinking ? \"Stop response\" : \"Send message\"}\n                    onClick={thinking ? stop : () => send(draft)}\n                  >\n                    {thinking ? (\n                      <Square className=\"size-3.5 fill-current\" />\n                    ) : (\n                      <ArrowUp />\n                    )}\n                  </Button>\n                </PromptInputAction>\n              </div>\n            </PromptInputActions>\n          </PromptInput>\n          <input\n            ref={fileInput}\n            type=\"file\"\n            accept=\".txt,.md,.csv\"\n            className=\"sr-only\"\n            aria-label=\"Context note file\"\n            onChange={(event) => {\n              void attach(event.currentTarget.files?.[0])\n              event.currentTarget.value = \"\"\n            }}\n          />\n          {notice && (\n            <p role=\"status\" className=\"text-muted-foreground mt-3 text-xs\">\n              {notice}\n            </p>\n          )}\n          {centered && (\n            <div className=\"mt-4 flex flex-wrap gap-2\">\n              {starters.map((text) => (\n                <PromptSuggestion\n                  key={text}\n                  size=\"sm\"\n                  className=\"h-auto min-h-8 rounded-lg text-xs font-normal whitespace-normal\"\n                  onClick={() => send(text)}\n                >\n                  {text}\n                </PromptSuggestion>\n              ))}\n            </div>\n          )}\n          {voice && (\n            <div className=\"mt-3 flex justify-end\">\n              <Button\n                variant=\"link\"\n                size=\"sm\"\n                disabled={muted || thinking}\n                onClick={() => send(sampleSpokenQuestion, \"voice\")}\n                className=\"text-muted-foreground h-7 px-0 text-xs\"\n              >\n                Try a sample voice turn\n              </Button>\n            </div>\n          )}\n        </motion.div>\n      </div>\n      <footer className=\"text-muted-foreground border-border flex shrink-0 flex-wrap items-center justify-between gap-2 border-t px-4 py-3 text-[11px] sm:px-8\">\n        <span>Interactive demo · sample replies · no microphone recording</span>\n        <details className=\"relative\">\n          <summary className=\"cursor-pointer\">Components used</summary>\n          <div className=\"bg-popover text-popover-foreground absolute right-0 bottom-full z-20 mb-3 w-64 rounded-lg border p-4 text-xs shadow-sm\">\n            <p className=\"mb-2 font-medium\">Composed from Agents Kit</p>\n            <ul className=\"space-y-2\">\n              <li>\n                Prompt Kit: input, messages, scrolling, suggestions, loader\n              </li>\n              <li>LiveKit: microphone toggle</li>\n              <li>OrbKit: Nimbus</li>\n              <li>Shared buttons and theme tokens</li>\n            </ul>\n          </div>\n        </details>\n      </footer>\n    </section>\n  )\n}\n"
    },
    {
      "path": "components/prompt-kit/chat-container.tsx",
      "target": "@components/prompt-kit/chat-container.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { StickToBottom } from \"use-stick-to-bottom\"\n\nexport type ChatContainerRootProps = {\n  children: React.ReactNode\n  className?: string\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport type ChatContainerContentProps = {\n  children: React.ReactNode\n  className?: string\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport type ChatContainerScrollAnchorProps = {\n  className?: string\n  ref?: React.RefObject<HTMLDivElement>\n} & React.HTMLAttributes<HTMLDivElement>\n\nfunction ChatContainerRoot({\n  children,\n  className,\n  ...props\n}: ChatContainerRootProps) {\n  return (\n    <StickToBottom\n      className={cn(\"flex overflow-y-auto\", className)}\n      resize=\"smooth\"\n      initial=\"instant\"\n      role=\"log\"\n      {...props}\n    >\n      {children}\n    </StickToBottom>\n  )\n}\n\nfunction ChatContainerContent({\n  children,\n  className,\n  ...props\n}: ChatContainerContentProps) {\n  return (\n    <StickToBottom.Content\n      className={cn(\"flex w-full flex-col\", className)}\n      {...props}\n    >\n      {children}\n    </StickToBottom.Content>\n  )\n}\n\nfunction ChatContainerScrollAnchor({\n  className,\n  ...props\n}: ChatContainerScrollAnchorProps) {\n  return (\n    <div\n      className={cn(\"h-px w-full shrink-0 scroll-mt-4\", className)}\n      aria-hidden=\"true\"\n      {...props}\n    />\n  )\n}\n\nexport { ChatContainerRoot, ChatContainerContent, ChatContainerScrollAnchor }\n"
    },
    {
      "path": "lib/utils.ts",
      "target": "@lib/utils.ts",
      "type": "registry:lib",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n\nexport function getSitePathname(\n  pathname: string,\n  basePath = process.env.NEXT_PUBLIC_BASE_PATH || \"\"\n) {\n  const base = basePath.replace(/\\/+$/, \"\")\n  const path =\n    pathname === base || pathname.startsWith(`${base}/`)\n      ? pathname.slice(base.length)\n      : pathname\n  return path.replace(/\\/+$/, \"\") || \"/\"\n}\n\n/**\n * Get the base URL depending on the current environment\n */\nexport function getBaseUrl() {\n  const basePath = (process.env.NEXT_PUBLIC_BASE_PATH || \"\").replace(/\\/$/, \"\")\n  const withBasePath = (url: string) => {\n    const base = url.replace(/\\/$/, \"\")\n    return basePath && !base.endsWith(basePath) ? `${base}${basePath}` : base\n  }\n  // For server-side rendering, we need to use environment variables\n  if (typeof window === \"undefined\") {\n    // Check for Vercel-specific environment variables\n    // Production URL takes precedence if available (works in all environments)\n    if (process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL) {\n      return withBasePath(\n        `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}`\n      )\n    }\n\n    // For branch deployments\n    if (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL) {\n      return withBasePath(\n        `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`\n      )\n    }\n\n    // For regular deployments\n    if (process.env.NEXT_PUBLIC_VERCEL_URL) {\n      return withBasePath(`https://${process.env.NEXT_PUBLIC_VERCEL_URL}`)\n    }\n\n    // Legacy support\n    if (process.env.VERCEL_URL) {\n      return withBasePath(`https://${process.env.VERCEL_URL}`)\n    }\n\n    // Fall back to explicitly set environment variables\n    if (process.env.NEXT_PUBLIC_BASE_URL) {\n      return withBasePath(process.env.NEXT_PUBLIC_BASE_URL)\n    }\n\n    // Default for development - check PORT env var first\n    const port = process.env.PORT || 3000\n    return withBasePath(\n      process.env.NODE_ENV === \"development\" ? `http://localhost:${port}` : \"\"\n    )\n  }\n\n  // For client-side, we can just use the browser's location\n  return withBasePath(window.location.origin)\n}\n"
    },
    {
      "path": "components/prompt-kit/loader.tsx",
      "target": "@components/prompt-kit/loader.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React from \"react\"\n\nexport interface LoaderProps {\n  variant?:\n    | \"circular\"\n    | \"classic\"\n    | \"pulse\"\n    | \"pulse-dot\"\n    | \"dots\"\n    | \"typing\"\n    | \"wave\"\n    | \"bars\"\n    | \"terminal\"\n    | \"text-blink\"\n    | \"text-shimmer\"\n    | \"loading-dots\"\n  size?: \"sm\" | \"md\" | \"lg\"\n  text?: string\n  className?: string\n}\n\nexport function CircularLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const sizeClasses = {\n    sm: \"size-4\",\n    md: \"size-5\",\n    lg: \"size-6\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"border-primary animate-spin rounded-full border-2 border-t-transparent\",\n        sizeClasses[size],\n        className\n      )}\n    >\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function ClassicLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const sizeClasses = {\n    sm: \"size-4\",\n    md: \"size-5\",\n    lg: \"size-6\",\n  }\n\n  const barSizes = {\n    sm: { height: \"6px\", width: \"1.5px\" },\n    md: { height: \"8px\", width: \"2px\" },\n    lg: { height: \"10px\", width: \"2.5px\" },\n  }\n\n  return (\n    <div className={cn(\"relative\", sizeClasses[size], className)}>\n      <div className=\"absolute h-full w-full\">\n        {[...Array(12)].map((_, i) => (\n          <div\n            key={i}\n            className=\"bg-primary absolute animate-[spinner-fade_1.2s_linear_infinite] rounded-full\"\n            style={{\n              top: \"0\",\n              left: \"50%\",\n              marginLeft:\n                size === \"sm\" ? \"-0.75px\" : size === \"lg\" ? \"-1.25px\" : \"-1px\",\n              transformOrigin: `${size === \"sm\" ? \"0.75px\" : size === \"lg\" ? \"1.25px\" : \"1px\"} ${size === \"sm\" ? \"10px\" : size === \"lg\" ? \"14px\" : \"12px\"}`,\n              transform: `rotate(${i * 30}deg)`,\n              opacity: 0,\n              animationDelay: `${i * 0.1}s`,\n              height: barSizes[size].height,\n              width: barSizes[size].width,\n            }}\n          />\n        ))}\n      </div>\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function PulseLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const sizeClasses = {\n    sm: \"size-4\",\n    md: \"size-5\",\n    lg: \"size-6\",\n  }\n\n  return (\n    <div className={cn(\"relative\", sizeClasses[size], className)}>\n      <div className=\"border-primary absolute inset-0 animate-[thin-pulse_1.5s_ease-in-out_infinite] rounded-full border-2\" />\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function PulseDotLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const sizeClasses = {\n    sm: \"size-1\",\n    md: \"size-2\",\n    lg: \"size-3\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"bg-primary animate-[pulse-dot_1.2s_ease-in-out_infinite] rounded-full\",\n        sizeClasses[size],\n        className\n      )}\n    >\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function DotsLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const dotSizes = {\n    sm: \"h-1.5 w-1.5\",\n    md: \"h-2 w-2\",\n    lg: \"h-2.5 w-2.5\",\n  }\n\n  const containerSizes = {\n    sm: \"h-4\",\n    md: \"h-5\",\n    lg: \"h-6\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center space-x-1\",\n        containerSizes[size],\n        className\n      )}\n    >\n      {[...Array(3)].map((_, i) => (\n        <div\n          key={i}\n          className={cn(\n            \"bg-primary animate-[bounce-dots_1.4s_ease-in-out_infinite] rounded-full\",\n            dotSizes[size]\n          )}\n          style={{\n            animationDelay: `${i * 160}ms`,\n          }}\n        />\n      ))}\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function TypingLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const dotSizes = {\n    sm: \"h-1 w-1\",\n    md: \"h-1.5 w-1.5\",\n    lg: \"h-2 w-2\",\n  }\n\n  const containerSizes = {\n    sm: \"h-4\",\n    md: \"h-5\",\n    lg: \"h-6\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center space-x-1\",\n        containerSizes[size],\n        className\n      )}\n    >\n      {[...Array(3)].map((_, i) => (\n        <div\n          key={i}\n          className={cn(\n            \"bg-primary animate-[typing_1s_infinite] rounded-full\",\n            dotSizes[size]\n          )}\n          style={{\n            animationDelay: `${i * 250}ms`,\n          }}\n        />\n      ))}\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function WaveLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const barWidths = {\n    sm: \"w-0.5\",\n    md: \"w-0.5\",\n    lg: \"w-1\",\n  }\n\n  const containerSizes = {\n    sm: \"h-4\",\n    md: \"h-5\",\n    lg: \"h-6\",\n  }\n\n  const heights = {\n    sm: [\"6px\", \"9px\", \"12px\", \"9px\", \"6px\"],\n    md: [\"8px\", \"12px\", \"16px\", \"12px\", \"8px\"],\n    lg: [\"10px\", \"15px\", \"20px\", \"15px\", \"10px\"],\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-0.5\",\n        containerSizes[size],\n        className\n      )}\n    >\n      {[...Array(5)].map((_, i) => (\n        <div\n          key={i}\n          className={cn(\n            \"bg-primary animate-[wave_1s_ease-in-out_infinite] rounded-full\",\n            barWidths[size]\n          )}\n          style={{\n            animationDelay: `${i * 100}ms`,\n            height: heights[size][i],\n          }}\n        />\n      ))}\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function BarsLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const barWidths = {\n    sm: \"w-1\",\n    md: \"w-1.5\",\n    lg: \"w-2\",\n  }\n\n  const containerSizes = {\n    sm: \"h-4 gap-1\",\n    md: \"h-5 gap-1.5\",\n    lg: \"h-6 gap-2\",\n  }\n\n  return (\n    <div className={cn(\"flex\", containerSizes[size], className)}>\n      {[...Array(3)].map((_, i) => (\n        <div\n          key={i}\n          className={cn(\n            \"bg-primary h-full animate-[wave-bars_1.2s_ease-in-out_infinite]\",\n            barWidths[size]\n          )}\n          style={{\n            animationDelay: `${i * 0.2}s`,\n          }}\n        />\n      ))}\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function TerminalLoader({\n  className,\n  size = \"md\",\n}: {\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const cursorSizes = {\n    sm: \"h-3 w-1.5\",\n    md: \"h-4 w-2\",\n    lg: \"h-5 w-2.5\",\n  }\n\n  const textSizes = {\n    sm: \"text-xs\",\n    md: \"text-sm\",\n    lg: \"text-base\",\n  }\n\n  const containerSizes = {\n    sm: \"h-4\",\n    md: \"h-5\",\n    lg: \"h-6\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center space-x-1\",\n        containerSizes[size],\n        className\n      )}\n    >\n      <span className={cn(\"text-primary font-mono\", textSizes[size])}>\n        {\">\"}\n      </span>\n      <div\n        className={cn(\n          \"bg-primary animate-[blink_1s_step-end_infinite]\",\n          cursorSizes[size]\n        )}\n      />\n      <span className=\"sr-only\">Loading</span>\n    </div>\n  )\n}\n\nexport function TextBlinkLoader({\n  text = \"Thinking\",\n  className,\n  size = \"md\",\n}: {\n  text?: string\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const textSizes = {\n    sm: \"text-xs\",\n    md: \"text-sm\",\n    lg: \"text-base\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"animate-[text-blink_2s_ease-in-out_infinite] font-medium\",\n        textSizes[size],\n        className\n      )}\n    >\n      {text}\n    </div>\n  )\n}\n\nexport function TextShimmerLoader({\n  text = \"Thinking\",\n  className,\n  size = \"md\",\n}: {\n  text?: string\n  className?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const textSizes = {\n    sm: \"text-xs\",\n    md: \"text-sm\",\n    lg: \"text-base\",\n  }\n\n  return (\n    <div\n      className={cn(\n        \"bg-[linear-gradient(to_right,var(--muted-foreground)_40%,var(--foreground)_60%,var(--muted-foreground)_80%)]\",\n        \"bg-size-[200%_auto] bg-clip-text font-medium text-transparent\",\n        \"animate-[shimmer_4s_infinite_linear]\",\n        textSizes[size],\n        className\n      )}\n    >\n      {text}\n    </div>\n  )\n}\n\nexport function TextDotsLoader({\n  className,\n  text = \"Thinking\",\n  size = \"md\",\n}: {\n  className?: string\n  text?: string\n  size?: \"sm\" | \"md\" | \"lg\"\n}) {\n  const textSizes = {\n    sm: \"text-xs\",\n    md: \"text-sm\",\n    lg: \"text-base\",\n  }\n\n  return (\n    <div\n      className={cn(\"inline-flex items-center\", className)}\n    >\n      <span className={cn(\"text-primary font-medium\", textSizes[size])}>\n        {text}\n      </span>\n      <span className=\"inline-flex\">\n        <span className=\"text-primary animate-[loading-dots_1.4s_infinite_0.2s]\">\n          .\n        </span>\n        <span className=\"text-primary animate-[loading-dots_1.4s_infinite_0.4s]\">\n          .\n        </span>\n        <span className=\"text-primary animate-[loading-dots_1.4s_infinite_0.6s]\">\n          .\n        </span>\n      </span>\n    </div>\n  )\n}\n\nfunction Loader({\n  variant = \"circular\",\n  size = \"md\",\n  text,\n  className,\n}: LoaderProps) {\n  switch (variant) {\n    case \"circular\":\n      return <CircularLoader size={size} className={className} />\n    case \"classic\":\n      return <ClassicLoader size={size} className={className} />\n    case \"pulse\":\n      return <PulseLoader size={size} className={className} />\n    case \"pulse-dot\":\n      return <PulseDotLoader size={size} className={className} />\n    case \"dots\":\n      return <DotsLoader size={size} className={className} />\n    case \"typing\":\n      return <TypingLoader size={size} className={className} />\n    case \"wave\":\n      return <WaveLoader size={size} className={className} />\n    case \"bars\":\n      return <BarsLoader size={size} className={className} />\n    case \"terminal\":\n      return <TerminalLoader size={size} className={className} />\n    case \"text-blink\":\n      return <TextBlinkLoader text={text} size={size} className={className} />\n    case \"text-shimmer\":\n      return <TextShimmerLoader text={text} size={size} className={className} />\n    case \"loading-dots\":\n      return <TextDotsLoader text={text} size={size} className={className} />\n    default:\n      return <CircularLoader size={size} className={className} />\n  }\n}\n\nexport { Loader }\n"
    },
    {
      "path": "components/prompt-kit/message.tsx",
      "target": "@components/prompt-kit/message.tsx",
      "type": "registry:component",
      "content": "import { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\nimport { Markdown } from \"./markdown\"\n\nexport type MessageProps = {\n  children: React.ReactNode\n  className?: string\n} & React.HTMLProps<HTMLDivElement>\n\nconst Message = ({ children, className, ...props }: MessageProps) => (\n  <div className={cn(\"flex gap-3\", className)} {...props}>\n    {children}\n  </div>\n)\n\nexport type MessageAvatarProps = {\n  src: string\n  alt: string\n  fallback?: string\n  delayMs?: number\n  className?: string\n}\n\nconst MessageAvatar = ({\n  src,\n  alt,\n  fallback,\n  delayMs,\n  className,\n}: MessageAvatarProps) => {\n  return (\n    <Avatar className={cn(\"h-8 w-8 shrink-0\", className)}>\n      <AvatarImage src={src} alt={alt} />\n      {fallback && (\n        <AvatarFallback delayMs={delayMs}>{fallback}</AvatarFallback>\n      )}\n    </Avatar>\n  )\n}\n\nexport type MessageContentProps = {\n  children: React.ReactNode\n  markdown?: boolean\n  className?: string\n} & React.ComponentProps<typeof Markdown> &\n  React.HTMLProps<HTMLDivElement>\n\nconst MessageContent = ({\n  children,\n  markdown = false,\n  className,\n  ...props\n}: MessageContentProps) => {\n  const classNames = cn(\n    \"prose prose-sm dark:prose-invert rounded-lg bg-secondary p-2 text-foreground break-words whitespace-normal\",\n    className\n  )\n\n  return markdown ? (\n    <Markdown className={classNames} {...props}>\n      {children as string}\n    </Markdown>\n  ) : (\n    <div className={classNames} {...props}>\n      {children}\n    </div>\n  )\n}\n\nexport type MessageActionsProps = {\n  children: React.ReactNode\n  className?: string\n} & React.HTMLProps<HTMLDivElement>\n\nconst MessageActions = ({\n  children,\n  className,\n  ...props\n}: MessageActionsProps) => (\n  <div\n    className={cn(\"text-muted-foreground flex items-center gap-2\", className)}\n    {...props}\n  >\n    {children}\n  </div>\n)\n\nexport type MessageActionProps = {\n  className?: string\n  tooltip: React.ReactNode\n  children: React.ReactNode\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\"\n} & React.ComponentProps<typeof Tooltip>\n\nconst MessageAction = ({\n  tooltip,\n  children,\n  className,\n  side = \"top\",\n  ...props\n}: MessageActionProps) => {\n  return (\n    <TooltipProvider>\n      <Tooltip {...props}>\n        <TooltipTrigger asChild>{children}</TooltipTrigger>\n        <TooltipContent side={side} className={className}>\n          {tooltip}\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  )\n}\n\nexport { Message, MessageAvatar, MessageContent, MessageActions, MessageAction }\n"
    },
    {
      "path": "components/ui/avatar.tsx",
      "target": "@components/ui/avatar.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"bg-muted flex size-full items-center justify-center rounded-full\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
    },
    {
      "path": "components/ui/tooltip.tsx",
      "target": "@components/ui/tooltip.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport * as React from \"react\"\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  )\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  )\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 4,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-gray-900 text-gray-50 dark:bg-gray-50 dark:text-gray-900 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-w-sm rounded-md px-3 py-1.5 text-xs\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"fill-gray-900 dark:fill-gray-50 z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
    },
    {
      "path": "components/prompt-kit/markdown.tsx",
      "target": "@components/prompt-kit/markdown.tsx",
      "type": "registry:component",
      "content": "import { cn } from \"@/lib/utils\"\nimport { marked } from \"marked\"\nimport { memo, useId, useMemo } from \"react\"\nimport ReactMarkdown, { Components } from \"react-markdown\"\nimport remarkBreaks from \"remark-breaks\"\nimport remarkGfm from \"remark-gfm\"\nimport { CodeBlock, CodeBlockCode } from \"./code-block\"\n\nexport type MarkdownProps = {\n  children: string\n  id?: string\n  className?: string\n  components?: Partial<Components>\n}\n\nfunction parseMarkdownIntoBlocks(markdown: string): string[] {\n  const tokens = marked.lexer(markdown)\n  return tokens.map((token) => token.raw)\n}\n\nfunction extractLanguage(className?: string): string {\n  if (!className) return \"plaintext\"\n  const match = className.match(/language-(\\w+)/)\n  return match ? match[1] : \"plaintext\"\n}\n\nconst INITIAL_COMPONENTS: Partial<Components> = {\n  code: function CodeComponent({ className, children, ...props }) {\n    const isInline =\n      !props.node?.position?.start.line ||\n      props.node?.position?.start.line === props.node?.position?.end.line\n\n    if (isInline) {\n      return (\n        <span\n          className={cn(\n            \"bg-primary-foreground rounded-sm px-1 font-mono text-sm\",\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </span>\n      )\n    }\n\n    const language = extractLanguage(className)\n\n    return (\n      <CodeBlock className={className}>\n        <CodeBlockCode code={children as string} language={language} />\n      </CodeBlock>\n    )\n  },\n  pre: function PreComponent({ children }) {\n    return <>{children}</>\n  },\n}\n\nconst MemoizedMarkdownBlock = memo(\n  function MarkdownBlock({\n    content,\n    components = INITIAL_COMPONENTS,\n  }: {\n    content: string\n    components?: Partial<Components>\n  }) {\n    return (\n      <ReactMarkdown\n        remarkPlugins={[remarkGfm, remarkBreaks]}\n        components={components}\n      >\n        {content}\n      </ReactMarkdown>\n    )\n  },\n  function propsAreEqual(prevProps, nextProps) {\n    return prevProps.content === nextProps.content\n  }\n)\n\nMemoizedMarkdownBlock.displayName = \"MemoizedMarkdownBlock\"\n\nfunction MarkdownComponent({\n  children,\n  id,\n  className,\n  components = INITIAL_COMPONENTS,\n}: MarkdownProps) {\n  const generatedId = useId()\n  const blockId = id ?? generatedId\n  const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])\n\n  return (\n    <div className={className}>\n      {blocks.map((block, index) => (\n        <MemoizedMarkdownBlock\n          key={`${blockId}-block-${index}`}\n          content={block}\n          components={components}\n        />\n      ))}\n    </div>\n  )\n}\n\nconst Markdown = memo(MarkdownComponent)\nMarkdown.displayName = \"Markdown\"\n\nexport { Markdown }\n"
    },
    {
      "path": "components/prompt-kit/code-block.tsx",
      "target": "@components/prompt-kit/code-block.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React, { useEffect, useState } from \"react\"\nimport { codeToHtml } from \"shiki\"\n\nexport type CodeBlockProps = {\n  children?: React.ReactNode\n  className?: string\n} & React.HTMLProps<HTMLDivElement>\n\nfunction CodeBlock({ children, className, ...props }: CodeBlockProps) {\n  return (\n    <div\n      className={cn(\n        \"not-prose flex w-full flex-col overflow-clip border\",\n        \"border-border bg-card text-card-foreground rounded-xl\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport type CodeBlockCodeProps = {\n  code: string\n  language?: string\n  theme?: string\n  className?: string\n} & React.HTMLProps<HTMLDivElement>\n\nfunction CodeBlockCode({\n  code,\n  language = \"tsx\",\n  theme = \"github-light\",\n  className,\n  ...props\n}: CodeBlockCodeProps) {\n  const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null)\n\n  useEffect(() => {\n    async function highlight() {\n      if (!code) {\n        setHighlightedHtml(\"<pre><code></code></pre>\")\n        return\n      }\n\n      const html = await codeToHtml(code, { lang: language, theme })\n      setHighlightedHtml(html)\n    }\n    highlight()\n  }, [code, language, theme])\n\n  const classNames = cn(\n    \"w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4\",\n    className\n  )\n\n  // SSR fallback: render plain code if not hydrated yet\n  return highlightedHtml ? (\n    <div\n      data-code-theme={theme}\n      className={classNames}\n      dangerouslySetInnerHTML={{ __html: highlightedHtml }}\n      {...props}\n    />\n  ) : (\n    <div data-code-theme={theme} className={classNames} {...props}>\n      <pre>\n        <code>{code}</code>\n      </pre>\n    </div>\n  )\n}\n\nexport type CodeBlockGroupProps = React.HTMLAttributes<HTMLDivElement>\n\nfunction CodeBlockGroup({\n  children,\n  className,\n  ...props\n}: CodeBlockGroupProps) {\n  return (\n    <div\n      className={cn(\"flex items-center justify-between\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport { CodeBlockGroup, CodeBlockCode, CodeBlock }\n"
    },
    {
      "path": "components/prompt-kit/prompt-input.tsx",
      "target": "@components/prompt-kit/prompt-input.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { Textarea } from \"@/components/ui/textarea\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\nimport React, {\n  createContext,\n  useContext,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\"\n\ntype PromptInputContextType = {\n  isLoading: boolean\n  value: string\n  setValue: (value: string) => void\n  maxHeight: number | string\n  onSubmit?: () => void\n  disabled?: boolean\n  textareaRef: React.RefObject<HTMLTextAreaElement | null>\n}\n\nconst PromptInputContext = createContext<PromptInputContextType>({\n  isLoading: false,\n  value: \"\",\n  setValue: () => {},\n  maxHeight: 240,\n  onSubmit: undefined,\n  disabled: false,\n  textareaRef: React.createRef<HTMLTextAreaElement>(),\n})\n\nfunction usePromptInput() {\n  return useContext(PromptInputContext)\n}\n\nexport type PromptInputProps = {\n  isLoading?: boolean\n  value?: string\n  onValueChange?: (value: string) => void\n  maxHeight?: number | string\n  onSubmit?: () => void\n  children: React.ReactNode\n  className?: string\n  disabled?: boolean\n} & React.ComponentProps<\"div\">\n\nfunction PromptInput({\n  className,\n  isLoading = false,\n  maxHeight = 240,\n  value,\n  onValueChange,\n  onSubmit,\n  children,\n  disabled = false,\n  onClick,\n  ...props\n}: PromptInputProps) {\n  const [internalValue, setInternalValue] = useState(value || \"\")\n  const textareaRef = useRef<HTMLTextAreaElement>(null)\n\n  const handleChange = (newValue: string) => {\n    setInternalValue(newValue)\n    onValueChange?.(newValue)\n  }\n\n  const handleClick: React.MouseEventHandler<HTMLDivElement> = (event) => {\n    if (!disabled) textareaRef.current?.focus()\n    onClick?.(event)\n  }\n\n  return (\n    <TooltipProvider>\n      <PromptInputContext.Provider\n        value={{\n          isLoading,\n          value: value ?? internalValue,\n          setValue: onValueChange ?? handleChange,\n          maxHeight,\n          onSubmit,\n          disabled,\n          textareaRef,\n        }}\n      >\n        <div\n          onClick={handleClick}\n          className={cn(\n            \"border-input bg-background cursor-text rounded-3xl border p-2 shadow-xs\",\n            disabled && \"cursor-not-allowed opacity-60\",\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n      </PromptInputContext.Provider>\n    </TooltipProvider>\n  )\n}\n\nexport type PromptInputTextareaProps = {\n  disableAutosize?: boolean\n} & React.ComponentProps<typeof Textarea>\n\nfunction PromptInputTextarea({\n  className,\n  onKeyDown,\n  disableAutosize = false,\n  ...props\n}: PromptInputTextareaProps) {\n  const { value, setValue, maxHeight, onSubmit, disabled, textareaRef } =\n    usePromptInput()\n\n  const adjustHeight = (element: HTMLTextAreaElement | null) => {\n    if (!element || disableAutosize) return\n    element.style.height = \"auto\"\n    element.style.height =\n      typeof maxHeight === \"number\"\n        ? `${Math.min(element.scrollHeight, maxHeight)}px`\n        : `min(${element.scrollHeight}px, ${maxHeight})`\n  }\n\n  const handleRef = (element: HTMLTextAreaElement | null) => {\n    textareaRef.current = element\n    adjustHeight(element)\n  }\n\n  useLayoutEffect(() => {\n    adjustHeight(textareaRef.current)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [value, maxHeight, disableAutosize])\n\n  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n    adjustHeight(event.target)\n    setValue(event.target.value)\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n      e.preventDefault()\n      onSubmit?.()\n    }\n    onKeyDown?.(e)\n  }\n\n  return (\n    <Textarea\n      ref={handleRef}\n      value={value}\n      onChange={handleChange}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"text-primary min-h-[44px] w-full resize-none border-none bg-transparent shadow-none outline-none focus-visible:ring-0 focus-visible:ring-offset-0\",\n        className\n      )}\n      rows={1}\n      disabled={disabled}\n      {...props}\n    />\n  )\n}\n\nexport type PromptInputActionsProps = React.HTMLAttributes<HTMLDivElement>\n\nfunction PromptInputActions({\n  children,\n  className,\n  ...props\n}: PromptInputActionsProps) {\n  return (\n    <div className={cn(\"flex items-center gap-2\", className)} {...props}>\n      {children}\n    </div>\n  )\n}\n\nexport type PromptInputActionProps = {\n  className?: string\n  tooltip: React.ReactNode\n  children: React.ReactNode\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\"\n} & React.ComponentProps<typeof Tooltip>\n\nfunction PromptInputAction({\n  tooltip,\n  children,\n  className,\n  side = \"top\",\n  ...props\n}: PromptInputActionProps) {\n  const { disabled } = usePromptInput()\n\n  return (\n    <Tooltip {...props}>\n      <TooltipTrigger\n        asChild\n        disabled={disabled}\n        onClick={(event) => event.stopPropagation()}\n      >\n        {children}\n      </TooltipTrigger>\n      <TooltipContent side={side} className={className}>\n        {tooltip}\n      </TooltipContent>\n    </Tooltip>\n  )\n}\n\nexport {\n  PromptInput,\n  PromptInputTextarea,\n  PromptInputActions,\n  PromptInputAction,\n}\n"
    },
    {
      "path": "components/ui/textarea.tsx",
      "target": "@components/ui/textarea.tsx",
      "type": "registry:component",
      "content": "import { cn } from \"@/lib/utils\"\nimport * as React from \"react\"\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive dark:aria-invalid:ring-destructive/50 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Textarea }\n"
    },
    {
      "path": "components/prompt-kit/prompt-suggestion.tsx",
      "target": "@components/prompt-kit/prompt-suggestion.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { VariantProps } from \"class-variance-authority\"\n\nexport type PromptSuggestionProps = {\n  children: React.ReactNode\n  variant?: VariantProps<typeof buttonVariants>[\"variant\"]\n  size?: VariantProps<typeof buttonVariants>[\"size\"]\n  className?: string\n  highlight?: string\n} & React.ButtonHTMLAttributes<HTMLButtonElement>\n\nfunction PromptSuggestion({\n  children,\n  variant,\n  size,\n  className,\n  highlight,\n  ...props\n}: PromptSuggestionProps) {\n  const isHighlightMode = highlight !== undefined && highlight.trim() !== \"\"\n  const content = typeof children === \"string\" ? children : \"\"\n\n  if (!isHighlightMode) {\n    return (\n      <Button\n        variant={variant || \"outline\"}\n        size={size || \"lg\"}\n        className={cn(\"rounded-full\", className)}\n        {...props}\n      >\n        {children}\n      </Button>\n    )\n  }\n\n  if (!content) {\n    return (\n      <Button\n        variant={variant || \"ghost\"}\n        size={size || \"sm\"}\n        className={cn(\n          \"w-full cursor-pointer justify-start rounded-xl py-2\",\n          \"hover:bg-accent\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </Button>\n    )\n  }\n\n  const trimmedHighlight = highlight.trim()\n  const contentLower = content.toLowerCase()\n  const highlightLower = trimmedHighlight.toLowerCase()\n  const shouldHighlight = contentLower.includes(highlightLower)\n\n  return (\n    <Button\n      variant={variant || \"ghost\"}\n      size={size || \"sm\"}\n      className={cn(\n        \"w-full cursor-pointer justify-start gap-0 rounded-xl py-2\",\n        \"hover:bg-accent\",\n        className\n      )}\n      {...props}\n    >\n      {shouldHighlight ? (\n        (() => {\n          const index = contentLower.indexOf(highlightLower)\n          if (index === -1)\n            return (\n              <span className=\"text-muted-foreground whitespace-pre-wrap\">\n                {content}\n              </span>\n            )\n\n          const actualHighlightedText = content.substring(\n            index,\n            index + highlightLower.length\n          )\n\n          const before = content.substring(0, index)\n          const after = content.substring(index + actualHighlightedText.length)\n\n          return (\n            <>\n              {before && (\n                <span className=\"text-muted-foreground whitespace-pre-wrap\">\n                  {before}\n                </span>\n              )}\n              <span className=\"text-primary font-medium whitespace-pre-wrap\">\n                {actualHighlightedText}\n              </span>\n              {after && (\n                <span className=\"text-muted-foreground whitespace-pre-wrap\">\n                  {after}\n                </span>\n              )}\n            </>\n          )\n        })()\n      ) : (\n        <span className=\"text-muted-foreground whitespace-pre-wrap\">\n          {content}\n        </span>\n      )}\n    </Button>\n  )\n}\n\nexport { PromptSuggestion }\n"
    },
    {
      "path": "components/ui/button.tsx",
      "target": "@components/ui/button.tsx",
      "type": "registry:component",
      "content": "import { cn } from \"@/lib/utils\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * as React from \"react\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n  }) {\n  const Comp = asChild ? Slot : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Button, buttonVariants }\n"
    },
    {
      "path": "components/ui/theme-toggle.tsx",
      "target": "@components/ui/theme-toggle.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { Button } from \"@/components/boardui/base/buttons/button\"\nimport { Moon, Sun } from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { useEffect, useState } from \"react\"\n\nexport function ThemeToggle() {\n  const { resolvedTheme, setTheme } = useTheme()\n  const [mounted, setMounted] = useState(false)\n  useEffect(() => setMounted(true), [])\n  if (!mounted) return <span aria-hidden className=\"inline-block size-8\" />\n  const dark = resolvedTheme === \"dark\"\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"small\"\n      iconOnly\n      leadingIcon={dark ? Sun : Moon}\n      aria-label={dark ? \"Use light theme\" : \"Use dark theme\"}\n      onClick={() => setTheme(dark ? \"light\" : \"dark\")}\n    />\n  )\n}\n"
    },
    {
      "path": "components/boardui/base/buttons/button.tsx",
      "target": "@components/boardui/base/buttons/button.tsx",
      "type": "registry:component",
      "content": "import { cx, sortCx } from \"@/components/boardui/utils/cx\"\nimport type {\n  AnchorHTMLAttributes,\n  ButtonHTMLAttributes,\n  ComponentType,\n  ReactNode,\n  Ref,\n} from \"react\"\nimport { Focusable } from \"react-aria-components\"\n\n/**\n * Figma source: Board UI → Buttons (node 3656:13819).\n *\n * Variant matrix from Figma:\n *   Type     = Primary | Secondary | Ghost | Danger\n *   Size     = Medium  | Small | Xs\n *   State    = Default | Hover | Active | Disabled        (CSS pseudo)\n *   OnlyIcon = false   | true\n *\n * Sizing (1:1 with Figma):\n *\n *                       Medium                    Small                     Xs\n *   container          h=36, p=8,   r=10         h=32, px=8 py=6, r=8      h=24, px=8, r=4\n *   gap                 2px                       2px                      1.33px→1\n *   icon                20×20                     18×18                    14×14\n *   label wrapper       px=4                      px=2                     px=2\n *   text style          Body 1/Medium             Body 1/Medium            Caption 1/Semibold\n *   icon-only square    36×36 (content-derived)   32×32 (forced size)      24×24 (forced size)\n *\n * `xs` is the smallest tier - first needed for the calendar template's\n * event-details modal (\"Join\" / edit-icon buttons, node 3920:10954), which\n * scales every dimension down by the same ~0.667 factor from Figma; the\n * table above rounds those to clean pixel values rather than reproducing\n * the fractional source numbers.\n *\n * Icons are rendered by the component itself via the `leadingIcon` /\n * `trailingIcon` props so the consumer can't pass the wrong size. Pass\n * a Remix Icon component reference (`RiAddLine`, not `<RiAddLine />`).\n *\n * For icon-only buttons:\n *   <Button iconOnly leadingIcon={RiAddLine} aria-label=\"Add\" />\n *\n * The HTML `type` prop is preserved; Figma's \"Type\" enum is renamed to\n * `variant` to avoid the clash.\n */\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\"\ntype ButtonSize = \"medium\" | \"small\" | \"xs\"\n\ntype IconComponent = ComponentType<{\n  className?: string\n  \"aria-hidden\"?: boolean | \"true\" | \"false\"\n}>\n\nexport interface ButtonProps\n  extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n  variant?: ButtonVariant\n  size?: ButtonSize\n  iconOnly?: boolean\n  leadingIcon?: IconComponent\n  trailingIcon?: IconComponent\n  children?: ReactNode\n  ref?: Ref<HTMLButtonElement>\n}\n\nexport interface ButtonLinkProps\n  extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"children\"> {\n  variant?: ButtonVariant\n  size?: ButtonSize\n  iconOnly?: boolean\n  leadingIcon?: IconComponent\n  trailingIcon?: IconComponent\n  children?: ReactNode\n  ref?: Ref<HTMLAnchorElement>\n}\n\nconst styles = sortCx({\n  base: [\n    \"inline-flex items-center justify-center gap-0.5 whitespace-nowrap overflow-hidden\",\n    \"font-sans select-none cursor-pointer\",\n    \"button-press-motion\",\n    \"outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-border-focus-ring\",\n    \"disabled:cursor-not-allowed aria-disabled:cursor-not-allowed\",\n  ].join(\" \"),\n\n  // Base shape per size (used when label is present OR medium icon-only).\n  size: {\n    medium: \"h-9 rounded-2lg p-2 text-body-medium\",\n    small: \"h-8 rounded-lg px-2 py-1.5 text-body-medium\",\n    xs: \"h-6 rounded-sm px-2 text-caption-1-semibold\",\n  },\n\n  // Icon-only override:\n  //   Medium → keep p-2; w expands from content (8+20+8 = 36) → square.\n  //   Small  → Figma forces 32×32 even though 8+18+8=34, so we hard-set size-8\n  //            and zero the padding; the inner flex centers the 18px icon.\n  //   Xs     → forces 24×24, content-centered - used for the calendar\n  //            template's edit-icon buttons (timezone/participants/reminder).\n  iconOnlySize: {\n    medium: \"\", // base size already produces 36×36 with a 20px icon\n    small: \"size-8 p-0\", // hard 32×32, content-centered\n    xs: \"size-6 p-0\", // hard 24×24, content-centered\n  },\n\n  icon: {\n    medium: \"size-5 shrink-0\", // 20px\n    small: \"size-[18px] shrink-0\", // 18px\n    xs: \"size-3.5 shrink-0\", // 14px\n  },\n\n  label: {\n    medium: \"inline-flex items-center justify-center px-1 shrink-0\", // px=4\n    small: \"inline-flex items-center justify-center px-0.5 shrink-0\", // px=2\n    xs: \"inline-flex items-center justify-center px-0.5 shrink-0\", // px=2\n  },\n\n  variant: {\n    primary: [\n      \"bg-button-primary\",\n      \"disabled:text-button-primary-disabled-foreground disabled:shadow-none\",\n      \"aria-disabled:text-button-primary-disabled-foreground aria-disabled:shadow-none\",\n    ].join(\" \"),\n    danger: [\n      \"bg-button-danger text-text-white shadow-xs\",\n      \"disabled:text-foreground-disabled-danger disabled:shadow-none\",\n      \"aria-disabled:text-foreground-disabled-danger aria-disabled:shadow-none\",\n    ].join(\" \"),\n    secondary: [\n      \"bg-background-primary-default text-text-primary\",\n      \"border border-border-button-default shadow-xs\",\n      \"hover:bg-background-primary-hover  hover:border-border-button-hover\",\n      \"active:bg-background-primary-active active:border-border-button-active\",\n      \"disabled:bg-background-primary-disabled disabled:border-border-button-default disabled:text-text-tertiary disabled:shadow-none\",\n      \"aria-disabled:bg-background-primary-disabled aria-disabled:border-border-button-default aria-disabled:text-text-tertiary aria-disabled:shadow-none\",\n    ].join(\" \"),\n    ghost: [\n      \"bg-button-ghost-background text-button-ghost-foreground\",\n      \"hover:bg-button-ghost-hover active:bg-button-ghost-active\",\n      \"disabled:bg-button-ghost-disabled disabled:text-button-ghost-disabled-foreground disabled:shadow-none\",\n      \"aria-disabled:bg-button-ghost-disabled aria-disabled:text-button-ghost-disabled-foreground aria-disabled:shadow-none\",\n    ].join(\" \"),\n  },\n})\n\nexport function Button({\n  variant = \"primary\",\n  size = \"medium\",\n  iconOnly = false,\n  leadingIcon: Leading,\n  trailingIcon: Trailing,\n  children,\n  className,\n  type = \"button\",\n  ref,\n  ...props\n}: ButtonProps) {\n  return (\n    <Focusable isDisabled={props.disabled}>\n      <button\n        ref={ref}\n        type={type}\n        className={cx(\n          styles.base,\n          styles.size[size],\n          styles.variant[variant],\n          iconOnly && styles.iconOnlySize[size],\n          className\n        )}\n        {...props}\n      >\n        {Leading ? <Leading className={styles.icon[size]} aria-hidden /> : null}\n        {iconOnly && !Leading && children != null ? (\n          <span\n            aria-hidden\n            className={cx(\n              styles.icon[size],\n              \"inline-flex items-center justify-center [&>svg]:size-full\"\n            )}\n          >\n            {children}\n          </span>\n        ) : null}\n        {!iconOnly && children !== undefined && children !== null && (\n          <span className={styles.label[size]}>{children}</span>\n        )}\n        {!iconOnly && Trailing ? (\n          <Trailing className={styles.icon[size]} aria-hidden />\n        ) : null}\n      </button>\n    </Focusable>\n  )\n}\n\n/** Anchor counterpart to Button for navigational actions. */\nexport function ButtonLink({\n  variant = \"primary\",\n  size = \"medium\",\n  iconOnly = false,\n  leadingIcon: Leading,\n  trailingIcon: Trailing,\n  children,\n  className,\n  ref,\n  ...props\n}: ButtonLinkProps) {\n  return (\n    <Focusable>\n      <a\n        ref={ref}\n        className={cx(\n          styles.base,\n          styles.size[size],\n          styles.variant[variant],\n          iconOnly && styles.iconOnlySize[size],\n          className\n        )}\n        {...props}\n      >\n        {Leading ? <Leading className={styles.icon[size]} aria-hidden /> : null}\n        {iconOnly && !Leading && children != null ? (\n          <span\n            aria-hidden\n            className={cx(\n              styles.icon[size],\n              \"inline-flex items-center justify-center [&>svg]:size-full\"\n            )}\n          >\n            {children}\n          </span>\n        ) : null}\n        {!iconOnly && children !== undefined && children !== null && (\n          <span className={styles.label[size]}>{children}</span>\n        )}\n        {!iconOnly && Trailing ? (\n          <Trailing className={styles.icon[size]} aria-hidden />\n        ) : null}\n      </a>\n    </Focusable>\n  )\n}\n\n/** Style maps, exported for advanced composition and the dev Design Tuner. */\nexport const buttonStyles = styles\n"
    },
    {
      "path": "components/boardui/utils/cx.ts",
      "target": "@components/boardui/utils/cx.ts",
      "type": "registry:component",
      "content": "import { extendTailwindMerge } from \"tailwind-merge\";\n\n/**\n * Text-style classes from styles/typography.css.\n *\n * IMPORTANT: every text-* utility we define via @theme (e.g. `text-body-medium`,\n * `text-title-1-semibold`) must be listed here. Otherwise tailwind-merge — which\n * has no view of our Tailwind theme — treats them as text-color utilities and\n * silently drops them when they appear in the same className as a real color\n * (`text-foreground-full`, `text-text-primary`, etc).\n *\n * If you add or rename a text style in typography.css, mirror the change here.\n */\nconst TEXT_FAMILIES = [\n  \"large-title\",\n  \"display-1\",\n  \"display-2\",\n  \"display-3\",\n  \"display-4\",\n  \"title-1\",\n  \"title-2\",\n  \"title-3\",\n  \"headline\",\n  \"body\",\n  \"body-2\",\n  \"caption-1\",\n  \"caption-2\",\n] as const;\n\nconst TEXT_WEIGHTS = [\"regular\", \"medium\", \"semibold\", \"bold\"] as const;\n\nconst TEXT_STYLE_SUFFIXES = TEXT_FAMILIES.flatMap((family) =>\n  TEXT_WEIGHTS.map((weight) => `${family}-${weight}`),\n);\n\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-size\": [{ text: TEXT_STYLE_SUFFIXES }],\n    },\n  },\n});\n\n/**\n * Merge Tailwind classes safely. Last-write-wins on conflicting utilities.\n */\nexport const cx = twMerge;\n\n/**\n * Identity helper that gives the Tailwind IntelliSense extension a hook for\n * sorting classes inside style objects (the extension doesn't sort inside\n * plain object literals otherwise).\n */\nexport function sortCx<\n  T extends Record<\n    string,\n    string | number | Record<string, string | number | Record<string, string | number>>\n  >,\n>(classes: T): T {\n  return classes;\n}\n"
    },
    {
      "path": "components/voice-agents/livekit/agent-track-toggle.tsx",
      "target": "@components/voice-agents/livekit/agent-track-toggle.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { Toggle } from \"@/components/voice-agents/livekit/_ui/toggle\"\nimport { cn } from \"@/lib/utils\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Track } from \"livekit-client\"\nimport {\n  LoaderIcon,\n  MicIcon,\n  MicOffIcon,\n  MonitorOffIcon,\n  MonitorUpIcon,\n  VideoIcon,\n  VideoOffIcon,\n} from \"lucide-react\"\nimport { Fragment, useMemo, useState, type ComponentProps } from \"react\"\n\nexport const agentTrackToggleVariants = cva([\"size-9\"], {\n  variants: {\n    size: {\n      default: \"h-9 px-2 min-w-9\",\n      sm: \"h-8 px-1.5 min-w-8\",\n      lg: \"h-10 px-2.5 min-w-10\",\n    },\n    variant: {\n      default: [\n        \"data-[state=off]:bg-destructive/10 data-[state=off]:text-destructive\",\n        \"data-[state=off]:hover:bg-destructive/15\",\n        \"data-[state=off]:focus-visible:ring-destructive/30\",\n        \"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n        \"data-[state=on]:hover:bg-foreground/10\",\n      ],\n      outline: [\n        \"data-[state=off]:bg-destructive/10 data-[state=off]:text-destructive data-[state=off]:border-destructive/20\",\n        \"data-[state=off]:hover:bg-destructive/15 data-[state=off]:hover:text-destructive\",\n        \"data-[state=off]:focus:text-destructive\",\n        \"data-[state=off]:focus-visible:border-destructive data-[state=off]:focus-visible:ring-destructive/30\",\n        \"data-[state=on]:hover:bg-foreground/10 data-[state=on]:hover:border-foreground/12\",\n        \"dark:data-[state=on]:hover:bg-foreground/10\",\n      ],\n    },\n  },\n  defaultVariants: {\n    variant: \"default\",\n  },\n})\n\nfunction getSourceIcon(\n  source: Track.Source,\n  enabled: boolean,\n  pending = false\n) {\n  if (pending) {\n    return LoaderIcon\n  }\n\n  switch (source) {\n    case Track.Source.Microphone:\n      return enabled ? MicIcon : MicOffIcon\n    case Track.Source.Camera:\n      return enabled ? VideoIcon : VideoOffIcon\n    case Track.Source.ScreenShare:\n      return enabled ? MonitorUpIcon : MonitorOffIcon\n    default:\n      return Fragment\n  }\n}\n\n/**\n * Props for the AgentTrackToggle component.\n */\nexport type AgentTrackToggleProps = VariantProps<\n  typeof agentTrackToggleVariants\n> &\n  ComponentProps<\"button\"> & {\n    /**\n     * The size of the toggle.\n     */\n    size?: \"sm\" | \"default\" | \"lg\"\n    /**\n     * The variant of the toggle.\n     * @defaultValue 'default'\n     */\n    variant?: \"default\" | \"outline\"\n    /**\n     * The track source to toggle (Microphone, Camera, or ScreenShare).\n     */\n    source: \"camera\" | \"microphone\" | \"screen_share\"\n    /**\n     * Whether the toggle is in a pending/loading state.\n     * When true, displays a loading spinner icon.\n     * @defaultValue false\n     */\n    pending?: boolean\n    /**\n     * Whether the toggle is currently pressed/enabled.\n     * @defaultValue false\n     */\n    pressed?: boolean\n    /**\n     * The default pressed state when uncontrolled.\n     * @defaultValue false\n     */\n    defaultPressed?: boolean\n    /**\n     * Callback fired when the pressed state changes.\n     */\n    onPressedChange?: (pressed: boolean) => void\n  }\n\n/**\n * A toggle button for controlling track publishing state.\n * Displays appropriate icons based on the track source and state.\n *\n * @extends ComponentProps<'button'>\n *\n * @example\n * ```tsx\n * <AgentTrackToggle\n *   source={Track.Source.Microphone}\n *   pressed={isMicEnabled}\n *   onPressedChange={(pressed) => setMicEnabled(pressed)}\n * />\n * ```\n */\nexport function AgentTrackToggle({\n  size = \"default\",\n  variant = \"default\",\n  source,\n  pending = false,\n  pressed,\n  defaultPressed = false,\n  className,\n  onPressedChange,\n  ...props\n}: AgentTrackToggleProps) {\n  const [uncontrolledPressed, setUncontrolledPressed] = useState(\n    defaultPressed ?? false\n  )\n  const isControlled = pressed !== undefined\n  const resolvedPressed = useMemo(\n    () => (isControlled ? pressed : uncontrolledPressed) ?? false,\n    [isControlled, pressed, uncontrolledPressed]\n  )\n  const IconComponent = getSourceIcon(\n    source as Track.Source,\n    resolvedPressed,\n    pending\n  )\n  const handlePressedChange = (nextPressed: boolean) => {\n    if (!isControlled) {\n      setUncontrolledPressed(nextPressed)\n    }\n    onPressedChange?.(nextPressed)\n  }\n\n  return (\n    <Toggle\n      size={size}\n      variant={variant}\n      pressed={isControlled ? pressed : undefined}\n      defaultPressed={isControlled ? undefined : defaultPressed}\n      aria-label={`Toggle ${source}`}\n      onPressedChange={handlePressedChange}\n      className={cn(\n        agentTrackToggleVariants({\n          size,\n          variant: variant ?? \"default\",\n          className,\n        })\n      )}\n      {...props}\n    >\n      <IconComponent className={cn(pending && \"animate-spin\")} />\n      {props.children}\n    </Toggle>\n  )\n}\n"
    },
    {
      "path": "components/voice-agents/livekit/_ui/toggle.tsx",
      "target": "@components/voice-agents/livekit/_ui/toggle.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Toggle as TogglePrimitive } from \"radix-ui\"\nimport * as React from \"react\"\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 min-w-9 px-2\",\n        sm: \"h-8 min-w-8 px-1.5\",\n        lg: \"h-10 min-w-10 px-2.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction Toggle({\n  className,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive.Root\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Toggle, toggleVariants }\n"
    },
    {
      "path": "components/voice-agents/orbkit/shdr-21.tsx",
      "target": "@components/voice-agents/orbkit/shdr-21.tsx",
      "type": "registry:component",
      "content": "/*\n * Deliberately not a `\"use client\"` module — see the note in `shdr-11.tsx`.\n *\n * Note for editors: the shader lives in a template literal, so its comments\n * must not contain backticks.\n */\nimport { ShaderOrb, type OrbVariant, type ShaderOrbProps } from \"./core\"\n\n/* ----------------------------------------------------------------------------\n   SHDR-21 — light diffusing through a cloud.\n\n   A real volumetric integration rather than a surface. The march walks a\n   density field bounded by a sphere and, at every step, does three things:\n\n     TRANSMITTANCE  how much of the background still gets through, tracked as\n                    T *= exp(-density * dt * absorb). Beer-Lambert.\n     SHADOW         a short second march toward the light, so the far side of a\n                    dense clump is dimmer than the lit side. This is the whole\n                    reason the orb reads as volume and not as a flat glow.\n     IN-SCATTER     light added at this step, weighted by density, by the shadow\n                    term, and by a Henyey-Greenstein phase function.\n\n   The phase function is what makes it feel like light rather than paint. It\n   biases scattering forward, so the limb facing the light blooms and the rest\n   stays soft — the same reason a cloud is blinding when you look toward the sun\n   through it and merely bright otherwise.\n\n   Alpha is 1 - T, which is exactly what the volume occludes, so this one needs\n   no radial fade to hide the canvas edge: density falls to zero at the sphere\n   boundary and the alpha goes with it.\n---------------------------------------------------------------------------- */\n\n/*\n * Step counts are `#define`s: ES 1.0 requires constant loop bounds. The march\n * is STEPS * (1 + LIGHT_STEPS) density evaluations, so LIGHT_STEPS is the\n * expensive knob — 4 is enough for readable self-shadowing.\n */\nconst NIMBUS_FRAG = `\n#define STEPS 56\n#define LIGHT_STEPS 4\n#define DENSITY_OCT 4\n#define AA 1\n\nconst float PI = 3.14159265359;\n\n// Volume-reactive values, resolved once per fragment in main().\nfloat nimbusPower;\nfloat nimbusDensity;\n\n/*\n  Density inside the sphere.\n\n  The radial term falls to zero at the boundary, which both bounds the volume\n  and gives the soft edge for free. The cos-warp folds the sample point a few\n  times — the same cheap turbulence the other orbs use — and the threshold\n  carves that into clumps rather than an even fog.\n*/\nfloat density(vec3 p, float animTime) {\n  float shell = 1.0 - length(p) / uP_radius;\n  if (shell <= 0.0) return 0.0;\n\n  vec3 q = p * uP_scale;\n  float f = 1.0;\n  for (int k = 0; k < DENSITY_OCT; k++) {\n    q += cos(q.yzx * f + animTime * uP_churn) / f;\n    f *= 1.8;\n  }\n\n  float n = (sin(q.x) + sin(q.y) + sin(q.z)) / 3.0 * 0.5 + 0.5;\n  // smoothstep against the threshold is the clump control: high threshold\n  // leaves sparse wisps, low fills the sphere with even fog\n  float clump = smoothstep(uP_threshold, 1.0, n);\n  return clump * pow(shell, uP_edgeSoft) * nimbusDensity;\n}\n\n/*\n  Henyey-Greenstein: g > 0 biases scattering forward, which is what gives the\n  bloom on the limb facing the light.\n\n  The physical form carries a 1/(4*PI) normalisation. It is dropped here and\n  folded into uP_power instead — kept in, the whole term sits around 0.02 and\n  the orb renders black unless power is pushed into the hundreds, which makes\n  the slider useless.\n*/\nfloat phaseHG(float c, float g) {\n  float g2 = g * g;\n  return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * c, 0.0001), 1.5);\n}\n\nvec4 nimbusRender(vec2 fragCoord) {\n  float animTime = uP_speed; // integrated clock\n\n  vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);\n  vec3 ro = vec3(0.0, 0.0, -uP_camDist);\n  vec3 rd = normalize(vec3(uv, uP_focal));\n\n  /*\n    Light direction, slowly orbiting so the shading is never static.\n\n    The z term is kept POSITIVE — the camera looks along +z, so a light also\n    pointing along +z sits behind the cloud. That is the back-lit case, where\n    dot(rd, L) approaches 1 and the forward-scattering phase blooms. Put the\n    light on the camera's side instead and every ray samples the phase function\n    on its back-scatter tail, where it is roughly ten times smaller, and the orb\n    goes muddy.\n  */\n  vec3 L = normalize(vec3(\n    cos(animTime * uP_lightSpin) * 0.7,\n    0.45,\n    sin(animTime * uP_lightSpin) * 0.35 + 0.65\n  ));\n\n  float phase = phaseHG(dot(rd, L), uP_aniso);\n\n  // Start the march at the sphere's front face instead of the camera — every\n  // step before that contributes nothing, and at 56 steps they are expensive.\n  float toCentre = uP_camDist;\n  float tStart = max(toCentre - uP_radius, 0.0);\n  float span = 2.0 * uP_radius;\n  float dt = span / float(STEPS);\n\n  float T = 1.0;\n  vec3 scattered = vec3(0.0);\n\n  for (int i = 0; i < STEPS; i++) {\n    float t = tStart + (float(i) + 0.5) * dt;\n    vec3 p = ro + rd * t;\n\n    float dn = density(p, animTime);\n    if (dn > 0.001) {\n      // short march toward the light for self-shadowing\n      float shadow = 1.0;\n      float lstep = uP_radius / float(LIGHT_STEPS);\n      for (int k = 1; k <= LIGHT_STEPS; k++) {\n        vec3 lp = p + L * (float(k) - 0.5) * lstep;\n        shadow *= exp(-density(lp, animTime) * lstep * uP_shadowAbsorb);\n      }\n\n      /*\n        In-scattered light: warm where lit, cool where the volume shadows\n        itself.\n\n        The shadow term appears ONCE, inside the mix. Multiplying by it again\n        as a factor — the obvious-looking thing to write — scales the shadowed\n        end of the mix toward zero, so the cool colour is always multiplied\n        away and the cloud comes out monochrome beige however it is tinted.\n        uP_shadowLift is how much light still reaches the shadowed side.\n      */\n      vec3 lit = mix(uC_shadow * uP_shadowLift, uC_light, shadow);\n      scattered += T * dn * dt * lit * phase * nimbusPower;\n\n      T *= exp(-dn * dt * uP_absorb);\n      if (T < 0.01) break;\n    }\n  }\n\n  // a soft ambient body so the unlit side is not pure black\n  float body = 1.0 - T;\n  scattered += uC_shadow * body * uP_ambient;\n\n  return vec4(scattered, body);\n}\n\nvoid main() {\n  /*\n    Agent output turns the light up; user input thickens the cloud. Both are\n    AMPLITUDES. Churn is deliberately NOT volume-scaled: it multiplies the\n    accumulated clock into a phase (animTime * churn), so scaling it by the\n    live volume would turn every volume wobble into a phase jump the size of\n    the whole clock — the cloud scrambles chaotically on each state change\n    instead of gliding, and gets worse the longer the page is open.\n  */\n  nimbusPower = uP_power * (0.7 + 0.9 * uOutput);\n  nimbusDensity = uP_density * (1.0 + 0.35 * uInput);\n\n  vec4 acc = vec4(0.0);\n#if AA > 1\n  for (int mx = 0; mx < AA; mx++) {\n    for (int my = 0; my < AA; my++) {\n      vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;\n      acc += nimbusRender(gl_FragCoord.xy + offset);\n    }\n  }\n  acc /= float(AA * AA);\n#else\n  acc = nimbusRender(gl_FragCoord.xy);\n#endif\n\n  vec3 col = tanh3(acc.rgb * uP_exposure);\n  float a = clamp(acc.a * uP_alphaGain, 0.0, 1.0);\n\n  // Emitted/scattered light, so rgb is already premultiplied — do NOT multiply\n  // by alpha again (see the same note in shdr-31).\n  gl_FragColor = vec4(col, a);\n}\n`\n\nexport const shdr21Orb: OrbVariant = {\n  key: \"shdr-21\",\n  label: \"SHDR-21\",\n  note: \"light diffusing through a cloud\",\n  frag: NIMBUS_FRAG,\n  params: [\n    {\n      key: \"speed\",\n      label: \"Anim speed\",\n      min: 0.015,\n      max: 10,\n      step: 0.05,\n      default: 10,\n      integrate: true,\n    },\n    {\n      key: \"camDist\",\n      label: \"Camera distance\",\n      min: 0.5,\n      max: 40,\n      step: 0.2,\n      default: 4.4,\n    },\n    { key: \"focal\", label: \"Lens\", min: 0.3, max: 15, step: 0.1, default: 1.8 },\n    {\n      key: \"radius\",\n      label: \"Cloud radius\",\n      min: 0.15,\n      max: 10,\n      step: 0.05,\n      default: 2,\n    },\n    {\n      key: \"scale\",\n      label: \"Cloud scale\",\n      min: 0.1,\n      max: 15,\n      step: 0.1,\n      default: 0.8,\n    },\n    { key: \"churn\", label: \"Churn\", min: 0, max: 5, step: 0.03, default: 0.3 },\n    {\n      key: \"threshold\",\n      label: \"Clumping\",\n      min: 0,\n      max: 3,\n      step: 0.015,\n      default: 0.075,\n    },\n    {\n      key: \"edgeSoft\",\n      label: \"Edge softness\",\n      min: 0.1,\n      max: 10,\n      step: 0.05,\n      default: 0.8,\n    },\n    {\n      key: \"density\",\n      label: \"Density\",\n      min: 0.03,\n      max: 20,\n      step: 0.1,\n      default: 3.2,\n    },\n    {\n      key: \"absorb\",\n      label: \"Absorption\",\n      min: 0.03,\n      max: 15,\n      step: 0.1,\n      default: 1.4,\n    },\n    {\n      key: \"shadowAbsorb\",\n      label: \"Shadow depth\",\n      min: 0,\n      max: 20,\n      step: 0.1,\n      default: 2.4,\n    },\n    {\n      key: \"shadowLift\",\n      label: \"Shadow lift\",\n      min: 0,\n      max: 5,\n      step: 0.03,\n      default: 0.55,\n    },\n    {\n      key: \"aniso\",\n      label: \"Forward scatter\",\n      min: -0.9,\n      max: 0.9,\n      step: 0.01,\n      default: 0.45,\n    },\n    {\n      key: \"lightSpin\",\n      label: \"Light orbit\",\n      min: 0,\n      max: 3,\n      step: 0.015,\n      default: 0.12,\n    },\n    {\n      key: \"power\",\n      label: \"Light power\",\n      min: 0.03,\n      max: 40,\n      step: 0.2,\n      default: 1.9,\n    },\n    {\n      key: \"ambient\",\n      label: \"Ambient\",\n      min: 0,\n      max: 3,\n      step: 0.015,\n      default: 0.12,\n    },\n    {\n      key: \"exposure\",\n      label: \"Exposure\",\n      min: 0.03,\n      max: 10,\n      step: 0.05,\n      default: 1,\n    },\n    {\n      key: \"alphaGain\",\n      label: \"Alpha gain\",\n      min: 0.05,\n      max: 10,\n      step: 0.05,\n      default: 1.5,\n    },\n  ],\n  /*\n   * The engine uploads these as uC_<key> vec3 uniforms. Warm light against a\n   * cool shadow is what reads as depth — a single-hue cloud looks flat however\n   * well it is shadowed.\n   */\n  colors: [\n    { key: \"light\", label: \"Light\", default: \"#ffd7a3\" },\n    { key: \"shadow\", label: \"Shadow\", default: \"#3a4a8c\" },\n  ],\n  statePresets: {\n    /*\n      Every state shares the same speed, geometry and cloud shape — only the\n      AMBIENCE and the palette move, so switching state relights the cloud\n      instead of restaging it. The engine glides params and cross-fades\n      colours on one shared easing, so the change reads as a mood shift.\n    */\n    idle: {\n      ambient: 0.12,\n      power: 1.9,\n      shadowLift: 0.55,\n    },\n    thinking: {\n      ambient: 0.22,\n      power: 2.15,\n      shadowLift: 0.65,\n    },\n    speaking: {\n      ambient: 0.46,\n      power: 3.1,\n      shadowLift: 0.95,\n    },\n  },\n  /*\n    The palette carries the rest of the state read: a warm lamp over cool\n    shadow at rest, shifting violet while it thinks, and burning hot while\n    speaking.\n  */\n  stateColors: {\n    idle: { light: \"#ffd7a3\", shadow: \"#3a4a8c\" },\n    thinking: { light: \"#e6d4ff\", shadow: \"#3b3f96\" },\n    speaking: { light: \"#ffb066\", shadow: \"#7a2f6e\" },\n  },\n}\n\nexport type Shdr21Props = Omit<ShaderOrbProps, \"variant\">\n\nexport function Shdr21({ size = 280, ...rest }: Shdr21Props) {\n  return <ShaderOrb variant={shdr21Orb} size={size} {...rest} />\n}\n\nexport default Shdr21\n"
    },
    {
      "path": "components/voice-agents/orbkit/core.tsx",
      "target": "@components/voice-agents/orbkit/core.tsx",
      "type": "registry:component",
      "content": "\"use client\"\n\nimport React, {\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\"\n\n/* ----------------------------------------------------------------------------\n   Orbkit core — raw WebGL shader orb runtime. No dependencies.\n\n   An orb is a full-screen triangle rendered into a transparent canvas by a\n   fragment shader. Every orb declares a parameter schema (sliders + colors);\n   the values are uploaded as uniforms each frame from a ref, so a controls\n   panel can tune them live without ever remounting the canvas (a remount would\n   drop the WebGL context).\n\n   Animation model: every state synthesizes two volume signals — input (user\n   speech energy) and output (agent speech energy) — smooths them, and the\n   shaders react to those. The flow clock's speed itself follows the output\n   volume, so orbs visibly quicken when the agent is talking.\n---------------------------------------------------------------------------- */\n\nexport type OrbState = \"idle\" | \"thinking\" | \"speaking\"\n\nexport const ORB_STATES = [\"idle\", \"thinking\", \"speaking\"] as const\n\nfunction clamp01(n: number) {\n  return Math.min(1, Math.max(0, n))\n}\n\n/** Per-state [input, output] volume synthesis. */\n/**\n * Transition rate shared by params, colours and the flow-speed multiplier.\n * They must move together: if the rate multiplier eases faster than the look,\n * a state change spins the orb up before it has finished cross-fading, which\n * reads as a lurch.\n *\n * This drives a critically damped spring rather than the exponential ease it\n * used to. An exponential's velocity is highest at the instant the target\n * changes, so every state change began with a jolt — and for params that are\n * spatial frequencies (Corona's warpFreq travels 5.25 -> 19.5 between states)\n * that jolt sweeps the field through its intermediate frequencies at maximum\n * rate, which is what read as the transition \"scrambling\".\n *\n * A spring starts at rest and accelerates, so the sweep is spread across the\n * transition instead of front-loaded. Measured on that warpFreq move it is a\n * 20% lower peak rate of change (20.3/s vs 25.3/s) AND it arrives sooner —\n * 1.50s to within 2% against the exponential's 2.18s, since an exponential\n * only ever asymptotes toward its target.\n */\nconst PARAM_EASE = 4\n\n/*\n  One step of a critically damped spring, implicit (semi-implicit Euler would\n  blow up at the frame times a backgrounded tab produces). Returns nothing and\n  writes through the scratch pair so the hot loop allocates nothing.\n*/\nconst springOut = { x: 0, v: 0 }\nfunction springStep(\n  x: number,\n  v: number,\n  target: number,\n  dt: number,\n  omega: number\n) {\n  const f = 1 + 2 * dt * omega\n  const oo = omega * omega\n  const hoo = dt * oo\n  const hhoo = dt * hoo\n  const detInv = 1 / (f + hhoo)\n  springOut.x = (f * x + dt * v + hhoo * target) * detInv\n  springOut.v = (v + hoo * (target - x)) * detInv\n}\n\nfunction targetVolumes(state: OrbState, t: number): [number, number] {\n  switch (state) {\n    case \"idle\":\n      return [0, 0.3]\n    case \"speaking\":\n      return [\n        clamp01(0.65 + Math.sin(t * 4.8) * 0.22),\n        clamp01(0.75 + Math.sin(t * 3.6) * 0.22),\n      ]\n    case \"thinking\": {\n      const base = 0.38 + 0.07 * Math.sin(t * 0.7)\n      const wander = 0.05 * Math.sin(t * 2.1) * Math.sin(t * 0.37 + 1.2)\n      return [\n        clamp01(base + wander),\n        clamp01(0.48 + 0.12 * Math.sin(t * 1.05 + 0.6)),\n      ]\n    }\n  }\n}\n\n/* ------------------------------ param schema ------------------------------- */\n\nexport interface OrbParamDef {\n  key: string\n  label: string\n  min: number\n  max: number\n  step: number\n  default: number\n  /**\n   * Rate params. The engine integrates them into a clock\n   * (`clock += dt * value * volumeSpeed`) and uploads the clock instead of the\n   * raw value, so changing the rate never jumps the phase — the motion speeds\n   * up or slows down rather than snapping to a new position.\n   */\n  integrate?: boolean\n}\n\nexport interface OrbColorDef {\n  key: string\n  label: string\n  /** hex, e.g. `#ff8b73` */\n  default: string\n}\n\nexport interface OrbVariant {\n  key: string\n  label: string\n  note: string\n  /** GLSL fragment shader body. Uniform declarations are generated for you. */\n  frag: string\n  params: OrbParamDef[]\n  colors: OrbColorDef[]\n  /**\n   * Per-state parameter targets. The engine glides each param toward the\n   * active state's preset. Params passed explicitly via the `params` prop\n   * always win over the preset.\n   */\n  statePresets?: Partial<Record<OrbState, Record<string, number>>>\n  /**\n   * Per-state colour targets, the colour counterpart of `statePresets`.\n   * Kept a separate map because presets are numeric and colours are hex\n   * strings — a union would lose type safety on both. Colours glide in RGB\n   * on the same easing as params, so a state change cross-fades rather\n   * than cutting. Colours passed explicitly via the `colors` prop always\n   * win, exactly as with params.\n   */\n  stateColors?: Partial<Record<OrbState, Record<string, string>>>\n}\n\nexport type OrbParamValues = Partial<Record<string, number>>\nexport type OrbColorValues = Partial<Record<string, string>>\n\n/** Every param and color at its schema default. */\nexport function defaultValuesFor(variant: OrbVariant): {\n  params: Record<string, number>\n  colors: Record<string, string>\n} {\n  return {\n    params: Object.fromEntries(variant.params.map((p) => [p.key, p.default])),\n    colors: Object.fromEntries(variant.colors.map((c) => [c.key, c.default])),\n  }\n}\n\nexport function hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim()\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]\n  const n = parseInt(h, 16)\n  if (h.length !== 6 || Number.isNaN(n)) return [1, 1, 1]\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]\n}\n\n/* ------------------------------- GLSL shared ------------------------------- */\n\nconst VERT = `\nattribute vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }\n`\n\n/**\n * Prelude prepended to every orb fragment shader: uniforms, value noise, fbm,\n * and the centered aspect-corrected UV helper.\n */\nexport const ORB_GLSL_HELPERS = `\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;   // slow ambient clock (half real-time)\nuniform float uAnim;   // flow clock — its speed follows the output volume\nuniform float uInput;  // input volume 0..1: user speech energy\nuniform float uOutput; // output volume 0..1: agent speech energy\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),\n    mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),\n    f.y\n  );\n}\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float a = 0.5;\n  for (int i = 0; i < 5; i++) {\n    v += a * noise(p);\n    p = p * 2.03 + vec2(11.7, 7.3);\n    a *= 0.5;\n  }\n  return v;\n}\nvec2 orbUV() { return (2.0 * gl_FragCoord.xy - uRes) / min(uRes.x, uRes.y); }\n\n// GLSL ES 1.0 has no tanh() — it arrived in ES 3.0. Shader-golf listings lean\n// on it as a tone-mapper, so it ships here. Clamped against exp() overflow;\n// accurate for the non-negative accumulators those shaders produce.\nvec3 tanh3(vec3 x) {\n  x = clamp(x, -10.0, 10.0);\n  vec3 e = exp(2.0 * x);\n  return (e - 1.0) / (e + 1.0);\n}\n\n`\n\nfunction paramUniformDecls(variant: OrbVariant): string {\n  return [\n    ...variant.params.map((p) => `uniform float uP_${p.key};`),\n    ...variant.colors.map((c) => `uniform vec3 uC_${c.key};`),\n  ].join(\"\\n\")\n}\n\n/* ------------------------------- engine ------------------------------------ */\n\n/**\n * Per-canvas context-lifecycle controller. Created on first mount of a canvas\n * and kept for the element's whole life — the router can hide a page and show\n * the same DOM again, and React re-runs effects on the same canvas, so the\n * lost/restored listeners must outlive any single effect run: an uncanceled\n * webglcontextlost event marks the context permanently unrestorable.\n */\ninterface CanvasContextController {\n  /** Whether a mounted orb currently wants this context alive. */\n  desired: boolean\n  /** Builds a render generation; returns its teardown. Rebound per effect run. */\n  start: (() => () => void) | null\n  /** Teardown of the live generation, if one is running. */\n  stopGen: (() => void) | null\n}\n\nconst canvasControllers = new WeakMap<\n  HTMLCanvasElement,\n  CanvasContextController\n>()\n\nfunction compile(\n  gl: WebGLRenderingContext,\n  type: number,\n  src: string\n): WebGLShader | null {\n  const shader = gl.createShader(type)\n  if (!shader) return null\n  gl.shaderSource(shader, src)\n  gl.compileShader(shader)\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(\"[orbkit] shader compile error:\", gl.getShaderInfoLog(shader))\n    gl.deleteShader(shader)\n    return null\n  }\n  return shader\n}\n\n/* ----------------------------------------------------------------------------\n   Wrappers — optional decoration drawn around, under and over the orb.\n\n   A wrapper is pure CSS/SVG, never a shader: the orb keeps its own canvas and\n   the decoration is composited on top of it by the browser. That keeps the\n   whole set free for any orb (no per-variant shader work), costs nothing on\n   the GPU budget the shaders are already spending, and means swapping one\n   wrapper for another at runtime never touches the WebGL context. Turning a\n   wrapper on or off does, since that is what moves the canvas into or out of\n   the wrapper element — see the effect's `wrapped` dependency.\n\n   Layout: a wrapped orb becomes a square box, and the canvas is absolutely\n   positioned inside it with the spec's `inset`. The FOOTPRINT is unchanged —\n   `size` is still the outer diameter — so dropping a wrapper onto an existing\n   orb reflows nothing; the orb itself just shrinks to leave the ring room.\n\n   Colour: every layer that isn't physically light or shadow paints in\n   `currentColor`, so a wrapper picks up the surrounding text colour and reads\n   correctly on light and dark pages with no configuration. `wrapperColor`\n   sets that colour when you want something other than the inherited one.\n---------------------------------------------------------------------------- */\n\nexport const ORB_WRAPPERS = [\n  \"none\",\n  \"glass\",\n  \"ring\",\n  \"dotted\",\n  \"ticks\",\n  \"reticle\",\n  \"grid\",\n  \"halftone\",\n  \"scanlines\",\n] as const\n\nexport type OrbWrapper = (typeof ORB_WRAPPERS)[number]\n\n/*\n  Keyframes for the animated wrappers, shipped inside the component so an orb\n  stays a single self-contained file with nothing to add to a global\n  stylesheet. React 19 hoists a <style href precedence> into <head> and\n  de-duplicates it, so a page full of wrapped orbs emits this exactly once;\n  older React renders it inline, which is redundant but harmless.\n\n  Reduced motion parks all of it. The shader runtime already honours the same\n  preference for the orb itself (see the reduce-motion branch in the render\n  loop), and a ring that keeps spinning around a frozen orb would be the worse\n  half of the two still moving.\n*/\nconst WRAPPER_STYLE_HREF = \"orbkit-wrapper\"\nconst WRAPPER_CSS = `\n@keyframes orbkit-w-spin { to { transform: rotate(360deg); } }\n@keyframes orbkit-w-roll { from { transform: translateY(-110%); } to { transform: translateY(360%); } }\n@media (prefers-reduced-motion: reduce) {\n  .orbkit-w-anim { animation: none !important; }\n}\n`\n\ninterface WrapperSpec {\n  /**\n   * How far the canvas sits inside the box, in percent, leaving the\n   * decoration room. Applied as explicit width/height rather than as `inset`:\n   * a canvas is a REPLACED element, so an absolutely positioned one with\n   * `left` and `right` both set does not stretch between them — it keeps its\n   * intrinsic size and the over-constrained edge is dropped. The orb would\n   * then be drawn into a canvas the size of the page.\n   */\n  inset: number\n  /**\n   * Soft circular mask on the canvas. Only the wrappers that read as a\n   * CONTAINER set one — a bubble has to hold the orb, whereas a bezel sits\n   * beside it and clipping the halo there would just amputate the glow.\n   */\n  mask?: string\n  /** Cast by the assembly as a whole, on the outer box. */\n  shadow?: string\n\n  /** True when the spec uses one of the keyframes above. */\n  animated?: boolean\n  /** Painted beneath the canvas. */\n  under?: ReactNode\n  /** Painted over it. */\n  over?: ReactNode\n}\n\nconst DISC: CSSProperties = { borderRadius: \"50%\" }\n\n/** One absolutely-positioned decoration layer, filling the wrapper box. */\nfunction Layer({\n  inset = 0,\n  style,\n  className,\n}: {\n  inset?: number | string\n  style: CSSProperties\n  className?: string\n}) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={className}\n      style={{ position: \"absolute\", inset, pointerEvents: \"none\", ...style }}\n    />\n  )\n}\n\n/**\n * A free-floating highlight — glass's specular and its bounce. These do not go\n * through `Layer` because they are placed with `left`/`top`/`width`, and a\n * style object that sets those on top of `Layer`'s `inset` shorthand is mixing\n * shorthand and longhand for the same property: React warns about it, and the\n * result depends on key order rather than on anything you would want to rely\n * on.\n */\nfunction Highlight({ style }: { style: CSSProperties }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      style={{\n        position: \"absolute\",\n        borderRadius: \"50%\",\n        pointerEvents: \"none\",\n        ...style,\n      }}\n    />\n  )\n}\n\n/**\n * The mask that makes a wrapper HOLD the orb: cuts the canvas back to the\n * bubble and stops a hairline short of the rim, so the orb never quite touches\n * the glass.\n *\n * The percentage has to be derived rather than written down. `closest-side`\n * measures the CANVAS, and a wrapper with a negative inset draws its canvas\n * LARGER than the bubble — at -7 the canvas is 114% of the box, so the rim\n * sits at 100/1.14 = 87.7% of the canvas's own radius, not at 100%.\n *\n * The gap is real pixels rather than a share of the size: it reads as the same\n * band at 120px and at 900px, which a percentage would not.\n */\nconst RIM_GAP_PX = 4\n\nfunction rimMask(inset: number): string {\n  const rim = (100 / (1 - (2 * inset) / 100)).toFixed(2)\n  // Feathered over the final pixel, so the cut is not a razor edge.\n  const solid = RIM_GAP_PX + 0.5\n  const clear = RIM_GAP_PX - 0.5\n  return `radial-gradient(circle closest-side, #000 calc(${rim}% - ${solid}px), rgba(0,0,0,0) calc(${rim}% - ${clear}px))`\n}\n\n/** Both spellings, since Safari still wants the prefix for mask-image. */\nfunction masked(image: string): CSSProperties {\n  return { WebkitMaskImage: image, maskImage: image }\n}\n\nconst svgLayer: CSSProperties = {\n  position: \"absolute\",\n  inset: 0,\n  width: \"100%\",\n  height: \"100%\",\n  pointerEvents: \"none\",\n  overflow: \"visible\",\n}\n\n/** Glass's overfill, shared by its inset and the mask derived from it. */\nconst GLASS_INSET = -4\n\nconst WRAPPER_SPECS: Record<Exclude<OrbWrapper, \"none\">, WrapperSpec> = {\n  /*\n    glass — a blown bubble with the orb suspended inside it.\n\n    Five layers in the order light actually arrives: the body brightening\n    toward the key light, the Fresnel ring where a sphere's edge turns almost\n    edge-on and reflects nearly everything, the rim itself, the window\n    reflection, and the bounce coming back up off whatever the bubble is\n    sitting on. All of it is white and black rather than `currentColor` —\n    glass has no colour of its own, only the light it moves around.\n  */\n  glass: {\n    /*\n      Negative on purpose. An orb's shader does not necessarily paint to the\n      edge of its canvas — most draw a sphere with transparent margin around\n      it — so a canvas sized to the bubble leaves a dead ring between the orb\n      and the rim, which is not what a thing suspended in glass looks like.\n      Oversizing the canvas by 14% pushes the sphere out to the rim, and\n      `rimMask` cuts whatever overflows — a few pixels short of the glass, so\n      the orb sits just inside it rather than welded to it. Orbs that already fill\n      their canvas lose a few percent off the limb, which is the same crop the\n      reference bubble makes.\n    */\n    inset: GLASS_INSET,\n    mask: rimMask(GLASS_INSET),\n    shadow: \"0 24px 48px -26px rgba(0,0,0,0.55)\",\n    over: (\n      <>\n        <Layer\n          style={{\n            ...DISC,\n            background:\n              \"radial-gradient(ellipse 80% 70% at 28% 20%, rgba(255,255,255,0.16), rgba(255,255,255,0.03) 45%, rgba(255,255,255,0) 72%)\",\n          }}\n        />\n        <Layer\n          style={{\n            ...DISC,\n            background:\n              \"radial-gradient(circle closest-side, rgba(255,255,255,0) 0%, rgba(255,255,255,0.0) 55%, rgba(255,255,255,0.05) 99.5%, rgba(255,255,255,0) 100%)\",\n            overflow: \"hidden\",\n          }}\n        />\n        <Layer\n          style={{\n            ...DISC,\n            boxShadow: \"3px 6px 10px #ffffff20 inset\",\n          }}\n        />\n        {/*\n          The shell's own darkening, just inside the rim. Invisible on a dark\n          page — it is black over black — and doing all the work on a light\n          one, where the white highlights below have nothing to stand out\n          against and the bubble would otherwise read as a bare drop shadow.\n        */}\n        <Layer\n          style={{\n            ...DISC,\n            background:\n              \"radial-gradient(circle closest-side, rgba(0,0,0,0) 78%, rgba(0,0,0,0.05) 93%, rgba(0,0,0,0.02) 100%)\",\n          }}\n        />\n        <Layer\n          style={{\n            ...DISC,\n            boxShadow:\n              \"inset 0 6px 12px -7px rgba(255,255,255,0.05), inset 0 -9px 16px -9px rgba(255,255,255,0.1), 0 0 0 1px rgba(0,0,0,0.07)\",\n          }}\n        />\n        <Highlight\n          style={{\n            left: \"15%\",\n            top: \"10%\",\n            width: \"38%\",\n            height: \"22%\",\n            transform: \"rotate(-25deg)\",\n            background:\n              \"radial-gradient(closest-side, rgba(255,255,255,0.9), rgba(255,255,255,0.3) 55%, rgba(255,255,255,0) 100%)\",\n            filter: \"blur(10px)\",\n          }}\n        />\n      </>\n    ),\n  },\n\n  /* ring — two hairlines and nothing else. The restrained one. */\n  ring: {\n    inset: 9,\n    over: (\n      <>\n        <Layer\n          style={{ ...DISC, border: \"1px solid currentColor\", opacity: 0.22 }}\n        />\n        <Layer\n          inset=\"5%\"\n          style={{ ...DISC, border: \"1px solid currentColor\", opacity: 0.1 }}\n        />\n      </>\n    ),\n  },\n\n  /*\n    dotted — evenly spaced dots around the circumference, turning slowly.\n\n    Drawn as one dashed circle with round caps and a near-zero dash length, so\n    each dash collapses to a dot. `pathLength=\"64\"` renormalizes the path to 64\n    units first, which is what makes the count exact: the dash period is\n    literally 1/64th of the circle, so the pattern closes on itself with no\n    seam where the last gap would otherwise be short.\n  */\n  dotted: {\n    inset: 10,\n    animated: true,\n    over: (\n      <svg\n        aria-hidden=\"true\"\n        viewBox=\"0 0 100 100\"\n        className=\"orbkit-w-anim\"\n        style={{ ...svgLayer, animation: \"orbkit-w-spin 48s linear infinite\" }}\n      >\n        <circle\n          cx=\"50\"\n          cy=\"50\"\n          r=\"47\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.7\"\n          strokeLinecap=\"round\"\n          pathLength={64}\n          strokeDasharray=\"0.0001 0.9999\"\n          opacity={0.45}\n        />\n      </svg>\n    ),\n  },\n\n  /*\n    ticks — an instrument bezel: a fine minor scale every 6 degrees with a\n    longer major tick every 30. Both are one repeating conic gradient masked\n    down to an annulus, so the tick count is set by the gradient's period and\n    the tick LENGTH by how far in the mask reaches.\n  */\n  ticks: {\n    inset: 12,\n    over: (\n      <>\n        <Layer\n          style={{\n            ...DISC,\n            opacity: 0.32,\n            background:\n              \"repeating-conic-gradient(from -0.5deg, transparent 0deg 0.2deg, currentColor 0.4deg 0.6deg, transparent 0.8deg 6deg)\",\n            ...masked(\n              \"radial-gradient(circle closest-side, transparent 88%, #000 90%, #000 97%, transparent 99%)\"\n            ),\n          }}\n        />\n        <Layer\n          style={{\n            ...DISC,\n            opacity: 0.6,\n            background:\n              \"repeating-conic-gradient(from -0.75deg, transparent 0deg 0.25deg, currentColor 0.5deg 1deg, transparent 1.25deg 30deg)\",\n            ...masked(\n              \"radial-gradient(circle closest-side, transparent 80%, #000 82%, #000 97%, transparent 99%)\"\n            ),\n          }}\n        />\n        <Layer\n          style={{ ...DISC, border: \"1px solid currentColor\", opacity: 0.12 }}\n        />\n      </>\n    ),\n  },\n\n  /* reticle — viewfinder furniture: corner brackets, cardinal ticks, a track. */\n  reticle: {\n    inset: 13,\n    over: (\n      <svg aria-hidden=\"true\" viewBox=\"0 0 100 100\" style={svgLayer}>\n        <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.2\" opacity=\"0.5\">\n          <path d=\"M1 13 L1 1 L13 1\" />\n          <path d=\"M87 1 L99 1 L99 13\" />\n          <path d=\"M99 87 L99 99 L87 99\" />\n          <path d=\"M13 99 L1 99 L1 87\" />\n        </g>\n        <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1\" opacity=\"0.38\">\n          <path d=\"M50 1 L50 9\" />\n          <path d=\"M50 91 L50 99\" />\n          <path d=\"M1 50 L9 50\" />\n          <path d=\"M91 50 L99 50\" />\n        </g>\n        <circle\n          cx=\"50\"\n          cy=\"50\"\n          r=\"46\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"0.7\"\n          opacity=\"0.22\"\n        />\n      </svg>\n    ),\n  },\n\n  /*\n    grid — a graticule laid over the orb and masked back to the disc, so the\n    mesh appears to be etched on the glass in front of it rather than drawn on\n    the page behind. The lines are 1px whatever the size; the SPACING is a\n    percentage, so the cell count holds from a gallery thumbnail to a\n    full-bleed hero.\n  */\n  grid: {\n    inset: 8,\n    /*\n      The ruling sits UNDER the canvas: graph paper the orb rests on, not a\n      mesh laid over its face. Drawn on top it crosshatched the shader — the\n      one thing the wrapper is meant to frame. The ring stays over, since it\n      only ever meets the transparent margin at the canvas edge.\n    */\n    under: (\n      <Layer\n        style={{\n          ...DISC,\n          opacity: 0.18,\n          backgroundImage:\n            \"repeating-linear-gradient(to right, currentColor 0 1px, transparent 1px 12.5%), repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px 12.5%)\",\n          ...masked(\n            \"radial-gradient(circle closest-side, #000 86%, rgba(0,0,0,0) 99%)\"\n          ),\n        }}\n      />\n    ),\n    over: (\n      <Layer\n        style={{ ...DISC, border: \"1px solid currentColor\", opacity: 0.2 }}\n      />\n    ),\n  },\n\n  /*\n    halftone — a print screen over the outer band of the orb. The mask keeps\n    the middle clear, so the dots read as the image breaking up toward its\n    edge instead of a texture pasted across the whole face.\n  */\n  halftone: {\n    inset: 6,\n    over: (\n      <Layer\n        style={{\n          ...DISC,\n          opacity: 0.5,\n          backgroundImage: \"radial-gradient(currentColor 22%, transparent 24%)\",\n          backgroundSize: \"7px 7px\",\n          ...masked(\n            \"radial-gradient(circle closest-side, transparent 40%, #000 80%, #000 94%, rgba(0,0,0,0) 100%)\"\n          ),\n        }}\n      />\n    ),\n  },\n\n  /*\n    scanlines — a phosphor tube. Black lines rather than `currentColor`,\n    because scanlines are the UNLIT gaps between rows and stay dark whatever\n    the page is; the slow bright band rolling down is the vertical hold\n    drifting, which is the part that reads as a CRT rather than as stripes.\n  */\n  scanlines: {\n    inset: 0,\n    animated: true,\n    over: (\n      <>\n        <Layer\n          style={{\n            ...DISC,\n            backgroundImage:\n              \"repeating-linear-gradient(to bottom, rgba(0,0,0,0.45) 0 1px, rgba(0,0,0,0) 1px 3px)\",\n            ...masked(\n              \"radial-gradient(circle closest-side, #000 84%, rgba(0,0,0,0) 100%)\"\n            ),\n          }}\n        />\n        <span\n          aria-hidden=\"true\"\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: \"50%\",\n            overflow: \"hidden\",\n            pointerEvents: \"none\",\n          }}\n        >\n          <span\n            className=\"orbkit-w-anim\"\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: 0,\n              height: \"30%\",\n              background:\n                \"linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,0.07) 50%, rgba(255,255,255,0) 100%)\",\n              animation: \"orbkit-w-roll 7s linear infinite\",\n            }}\n          />\n        </span>\n        <Layer\n          style={{ ...DISC, boxShadow: \"inset 0 0 40px -8px rgba(0,0,0,0.5)\" }}\n        />\n      </>\n    ),\n  },\n}\n\nexport interface ShaderOrbProps {\n  /** The orb definition: shader + param schema + state presets. */\n  variant: OrbVariant\n  /** Drives the synthesized volume signals. Defaults to `\"idle\"`. */\n  state?: OrbState\n  /** Rendered size in CSS pixels. Ignored when `className` sizes the canvas. */\n  size?: number\n  /** Explicit param overrides. Any key present here wins over the state preset. */\n  params?: OrbParamValues\n  /** Explicit color overrides, as hex strings. */\n  colors?: OrbColorValues\n  /**\n   * Per-state parameter targets, overriding the variant's own. Merged KEY BY\n   * KEY over what the orb already defines, so `{ thinking: { churn: 1.62 } }`\n   * retouches one param of one state and leaves every other param — and the\n   * other two states — exactly as the orb ships them.\n   *\n   * This is the prop form of the variant's `statePresets`, so you can retune\n   * an orb's states from the outside without forking its file. Values still\n   * glide, so switching states cross-fades into your targets. An explicit\n   * `params` value outranks this, the same way it outranks the variant.\n   */\n  statePresets?: Partial<Record<OrbState, Record<string, number>>>\n  /** The colour counterpart of `statePresets`, merged the same key-by-key way. */\n  stateColors?: Partial<Record<OrbState, Record<string, string>>>\n  /**\n   * Per-state volume drive, the third member of the same family. Use it to\n   * give each state its own energy; use `volumes` below instead when you have\n   * a real signal to feed in, such as live mic level.\n   */\n  stateVolumes?: Partial<Record<OrbState, { input?: number; output?: number }>>\n  /**\n   * Overrides the synthesized volume signals for the active state. The engine\n   * normally derives these from `state` — a slow breath at idle, a restless\n   * wander while thinking, speech-shaped peaks while speaking — and most\n   * shaders read them as their reactivity. Setting either channel here pins\n   * it instead, which is how the playground lets you dial each state's drive\n   * independently. Omit a channel to keep its synthesized motion.\n   */\n  volumes?: { input?: number; output?: number }\n  /** Freeze the animation on the current frame. */\n  paused?: boolean\n  /**\n   * Stop rendering while the orb is scrolled out of view. Defaults to `true` —\n   * a page full of orbs would otherwise run a WebGL loop per card.\n   */\n  pauseOffscreen?: boolean\n  /** Device-pixel-ratio ceiling. Defaults to `2`. */\n  maxDpr?: number\n  /**\n   * Decoration drawn around the orb — a glass bubble, a dotted bezel, a\n   * viewfinder. Defaults to `\"none\"`, which renders the bare canvas exactly as\n   * it always has, with no extra element in the tree.\n   *\n   * A wrapper never changes the orb's footprint: `size` stays the outer\n   * diameter and the canvas is inset inside it, so switching one on reflows\n   * nothing around it.\n   */\n  wrapper?: OrbWrapper\n  /**\n   * The colour a wrapper draws its lines and dots in. Defaults to\n   * `currentColor` — the inherited text colour — which is what makes the\n   * bezels legible on a light and a dark page without being told which one\n   * they are on. `glass` ignores it: glass has no colour of its own.\n   */\n  wrapperColor?: string\n  /** Applied to the outermost element — the wrapper when there is one. */\n  className?: string\n  /** Merged onto the outermost element's style. */\n  style?: CSSProperties\n  /** Accessible label. When omitted the orb is hidden from assistive tech. */\n  ariaLabel?: string\n}\n\nexport function ShaderOrb({\n  variant,\n  state = \"idle\",\n  size,\n  params,\n  colors,\n  statePresets,\n  stateColors,\n  stateVolumes,\n  volumes,\n  paused = false,\n  pauseOffscreen = true,\n  maxDpr = 2,\n  wrapper = \"none\",\n  wrapperColor,\n  className,\n  style,\n  ariaLabel,\n}: ShaderOrbProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const spec = wrapper === \"none\" ? undefined : WRAPPER_SPECS[wrapper]\n  const wrapped = spec !== undefined\n\n  // Live refs: the render loop reads these every frame, so changing a param\n  // never re-runs the GL setup effect (which would drop the context). Synced in\n  // an effect rather than during render — a ref write during render is unsafe\n  // under concurrent rendering, and the loop picks the new value up on the very\n  // next frame anyway.\n  /*\n    Whether the canvas has drawn a frame yet, tracked per variant because a\n    variant swap mounts a brand new canvas (see the `key` below).\n\n    A mounted-but-never-drawn canvas is at the browser's mercy: rather than\n    the transparent rectangle you would expect, a page that mounts a dozen at\n    once gets white boxes — and in some browsers a broken-image placeholder —\n    for as long as the compositor has nothing to raster. That window is not\n    small here: every orb compiles a full fragment shader synchronously in its\n    own mount effect, so on the gallery grid the first canvases sit empty\n    while the last ones are still compiling. Holding each canvas invisible\n    until its own first frame lands is what makes the grid fade in cleanly\n    instead of flashing. One state change per orb, once, on mount.\n  */\n  const [paintedKey, setPaintedKey] = useState<string | null>(null)\n  const painted = paintedKey === variant.key\n\n  const stateRef = useRef<OrbState>(state)\n  const paramsRef = useRef<OrbParamValues | undefined>(params)\n  const colorsRef = useRef<OrbColorValues | undefined>(colors)\n  const statePresetsRef = useRef(statePresets)\n  const stateColorsRef = useRef(stateColors)\n  const stateVolumesRef = useRef(stateVolumes)\n  const volumesRef = useRef(volumes)\n  const pausedRef = useRef(paused)\n\n  useEffect(() => {\n    stateRef.current = state\n    paramsRef.current = params\n    colorsRef.current = colors\n    statePresetsRef.current = statePresets\n    stateColorsRef.current = stateColors\n    stateVolumesRef.current = stateVolumes\n    volumesRef.current = volumes\n    pausedRef.current = paused\n  }, [\n    state,\n    params,\n    colors,\n    statePresets,\n    stateColors,\n    stateVolumes,\n    volumes,\n    paused,\n  ])\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const gl = canvas.getContext(\"webgl\", {\n      alpha: true,\n      // No MSAA: the geometry is a single full-screen triangle, so there are no\n      // primitive edges to antialias — softness comes from the shaders. Leaving\n      // it on costs the multisample buffers plus a resolve every frame.\n      antialias: false,\n      premultipliedAlpha: true,\n    })\n    if (!gl) return\n\n    const loseExt = gl.getExtension(\"WEBGL_lose_context\")\n\n    /*\n      A \"generation\" is everything tied to a live context: program, buffers,\n      observers, render loop. Browsers cap live WebGL contexts per page and\n      evict the oldest past the cap, and an evicted orb's canvas stays blank\n      forever unless the app rebuilds — so generations tear down and rebuild on\n      the lost/restored events instead of assuming the context is immortal.\n    */\n    let announcedPaint = false\n\n    const startGeneration = (): (() => void) => {\n      if (gl.isContextLost()) return () => {}\n      // Every generation announces its own first frame: a context that was\n      // lost and restored has an empty drawing buffer and is hidden again\n      // (below), so it has to earn its reveal back.\n      announcedPaint = false\n\n      const vs = compile(gl, gl.VERTEX_SHADER, VERT)\n      const fs = compile(\n        gl,\n        gl.FRAGMENT_SHADER,\n        ORB_GLSL_HELPERS + paramUniformDecls(variant) + variant.frag\n      )\n      const releaseShaders = () => {\n        if (vs) gl.deleteShader(vs)\n        if (fs) gl.deleteShader(fs)\n      }\n      if (!vs || !fs) return releaseShaders\n\n      const prog = gl.createProgram()\n      if (!prog) return releaseShaders\n      gl.attachShader(prog, vs)\n      gl.attachShader(prog, fs)\n      gl.linkProgram(prog)\n      if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n        console.error(\n          \"[orbkit] program link error:\",\n          gl.getProgramInfoLog(prog)\n        )\n        gl.deleteProgram(prog)\n        return releaseShaders\n      }\n      gl.useProgram(prog)\n\n      const buf = gl.createBuffer()\n      gl.bindBuffer(gl.ARRAY_BUFFER, buf)\n      gl.bufferData(\n        gl.ARRAY_BUFFER,\n        new Float32Array([-1, -1, 3, -1, -1, 3]),\n        gl.STATIC_DRAW\n      )\n      const aPos = gl.getAttribLocation(prog, \"aPos\")\n      gl.enableVertexAttribArray(aPos)\n      gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0)\n\n      gl.enable(gl.BLEND)\n      gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)\n\n      const uRes = gl.getUniformLocation(prog, \"uRes\")\n      const uTime = gl.getUniformLocation(prog, \"uTime\")\n      const uAnim = gl.getUniformLocation(prog, \"uAnim\")\n      const uInput = gl.getUniformLocation(prog, \"uInput\")\n      const uOutput = gl.getUniformLocation(prog, \"uOutput\")\n\n      const paramLocs = variant.params.map((p) => ({\n        def: p,\n        loc: gl.getUniformLocation(prog, `uP_${p.key}`),\n      }))\n      const colorLocs = variant.colors.map((c) => ({\n        def: c,\n        loc: gl.getUniformLocation(prog, `uC_${c.key}`),\n      }))\n\n      /* --- sizing: track the element box, not a one-shot measurement ------- */\n      // Backing-store scale, stepped down by the adaptive-resolution logic in\n      // the loop when the GPU can't hold frame rate. CSS size never changes —\n      // the browser upscales, which these soft shaders absorb gracefully.\n      let resScale = 1\n      const resize = () => {\n        const dpr = Math.min(window.devicePixelRatio || 1, maxDpr) * resScale\n        const w = Math.max(1, Math.round(canvas.clientWidth * dpr))\n        const h = Math.max(1, Math.round(canvas.clientHeight * dpr))\n        if (canvas.width !== w || canvas.height !== h) {\n          canvas.width = w\n          canvas.height = h\n          gl.viewport(0, 0, w, h)\n        }\n        /*\n          Uploaded UNCONDITIONALLY, outside the size guard. A rebuilt\n          generation (React strict-mode remount, a restored context) links a\n          fresh program whose uRes starts at zero — and the canvas usually\n          already holds the right backing size, so an upload gated behind\n          the resize never ran. With uRes = 0, orbUV() divides by zero and\n          every fragment lands transparent: a healthy context, a bound\n          program, and a permanently blank orb.\n        */\n        gl.uniform2f(uRes, w, h)\n      }\n      resize()\n\n      const resizeObserver =\n        typeof ResizeObserver !== \"undefined\"\n          ? new ResizeObserver(resize)\n          : null\n      resizeObserver?.observe(canvas)\n\n      /* --- visibility: don't burn a render loop on an offscreen orb -------- */\n      let visible = !pauseOffscreen\n      const intersectionObserver =\n        pauseOffscreen && typeof IntersectionObserver !== \"undefined\"\n          ? new IntersectionObserver(\n              (entries) => {\n                visible = Boolean(entries[0]?.isIntersecting)\n                if (visible) {\n                  last = performance.now() / 1000\n                }\n              },\n              { rootMargin: \"150px 0px\", threshold: 0 }\n            )\n          : null\n      if (intersectionObserver) {\n        intersectionObserver.observe(canvas)\n      } else {\n        visible = true\n      }\n\n      const reduceMotion =\n        typeof window.matchMedia === \"function\" &&\n        window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n      /* --- driver state ---------------------------------------------------- */\n      let tSec = 0\n      // random phase so two orbs on the same page never look synchronized\n      let anim = Math.random() * 100\n      let speed = 0.1\n      const cur = { in: 0, out: 0.3 }\n      const presets = variant.statePresets\n      const paramCur: Record<string, number> = {}\n      const paramVel: Record<string, number> = {}\n      const paramClocks: Record<string, number> = {}\n      const colorCur: Record<string, [number, number, number]> = {}\n      const colorVel: Record<string, [number, number, number]> = {}\n      let speedVel = 0\n      const [initialIn, initialOut] = targetVolumes(stateRef.current, 0)\n      cur.in = initialIn\n      cur.out = initialOut\n      let last = performance.now() / 1000\n      let raf = 0\n      // smoothed frame time for the adaptive-resolution check\n      let frameEma = 1 / 60\n\n      const uploadAndDraw = (dt: number, snap = false) => {\n        // Synthesized from the state, unless a channel is pinned via\n        // `volumes`. Pinned values still glide on the same easing, so dialing\n        // one in the playground cross-fades rather than jumping.\n        const [tin, tout] = targetVolumes(stateRef.current, tSec)\n        // Same order as params and colours: direct prop, then the per-state\n        // map, then the engine's own synthesis.\n        const liveVolumes = volumesRef.current\n        const stateVolume = stateVolumesRef.current?.[stateRef.current]\n        const targetIn = liveVolumes?.input ?? stateVolume?.input ?? tin\n        const targetOut = liveVolumes?.output ?? stateVolume?.output ?? tout\n        const kVol = 1 - Math.exp(-dt * 12)\n        cur.in += (targetIn - cur.in) * kVol\n        cur.out += (targetOut - cur.out) * kVol\n\n        /*\n          Flow speed follows the output volume. It multiplies every integrated\n          clock's increment, so it is a RATE: easing it quickly makes the orb\n          visibly lurch — a state change would spin the orb up hard before the\n          params had finished gliding.\n\n          It is therefore eased on the same constant as the params below, so a\n          state change ramps its motion over the same half second that its look\n          takes to cross-fade. The steady-state values are unchanged, so a\n          speaking orb still flows faster than an idle one; only the transition\n          into that rate is gradual.\n        */\n        const targetSpeed = 0.1 + (1 - Math.pow(cur.out - 1, 2)) * 0.9\n        if (snap) {\n          speed = targetSpeed\n          speedVel = 0\n        } else {\n          springStep(speed, speedVel, targetSpeed, dt, PARAM_EASE)\n          speed = springOut.x\n          speedVel = springOut.v\n        }\n        anim += dt * speed\n\n        gl.uniform1f(uTime, tSec * 0.5)\n        gl.uniform1f(uAnim, anim)\n        gl.uniform1f(uInput, cur.in)\n        gl.uniform1f(uOutput, cur.out)\n\n        // Resolution order per param: explicit `params` → `statePresets`\n        // prop → the variant's own preset → schema default. The two middle\n        // steps are per-key, so overriding one param of one state leaves the\n        // rest of that state alone. Values glide rather than snap.\n        const liveParams = paramsRef.current\n        const statePreset = presets?.[stateRef.current]\n        const overridePreset = statePresetsRef.current?.[stateRef.current]\n\n        for (const { def, loc } of paramLocs) {\n          const explicit = liveParams?.[def.key]\n          const target =\n            typeof explicit === \"number\"\n              ? explicit\n              : (overridePreset?.[def.key] ??\n                statePreset?.[def.key] ??\n                def.default)\n          const curVal = paramCur[def.key] ?? target\n          let next: number\n          if (snap) {\n            next = target\n            paramVel[def.key] = 0\n          } else {\n            springStep(curVal, paramVel[def.key] ?? 0, target, dt, PARAM_EASE)\n            next = springOut.x\n            paramVel[def.key] = springOut.v\n          }\n          paramCur[def.key] = next\n\n          if (def.integrate) {\n            const clock =\n              (paramClocks[def.key] ??\n                (paramClocks[def.key] = Math.random() * 100)) +\n              dt * speed * next\n            paramClocks[def.key] = clock\n            gl.uniform1f(loc, clock)\n          } else {\n            gl.uniform1f(loc, next)\n          }\n        }\n\n        // Same resolution order and same easing as params, so a state change\n        // cross-fades the palette instead of cutting to it.\n        const liveColors = colorsRef.current\n        const stateColor = variant.stateColors?.[stateRef.current]\n        const overrideColor = stateColorsRef.current?.[stateRef.current]\n        for (const { def, loc } of colorLocs) {\n          const target = hexToRgb(\n            liveColors?.[def.key] ??\n              overrideColor?.[def.key] ??\n              stateColor?.[def.key] ??\n              def.default\n          )\n          const curCol = (colorCur[def.key] ??= [...target] as [\n            number,\n            number,\n            number,\n          ])\n          const velCol = (colorVel[def.key] ??= [0, 0, 0])\n          for (let i = 0; i < 3; i++) {\n            if (snap) {\n              curCol[i] = target[i]\n              velCol[i] = 0\n            } else {\n              springStep(curCol[i], velCol[i], target[i], dt, PARAM_EASE)\n              curCol[i] = springOut.x\n              velCol[i] = springOut.v\n            }\n          }\n          gl.uniform3f(loc, curCol[0], curCol[1], curCol[2])\n        }\n\n        gl.clearColor(0, 0, 0, 0)\n        gl.clear(gl.COLOR_BUFFER_BIT)\n        gl.drawArrays(gl.TRIANGLES, 0, 3)\n        // Deferred a microtask: the first of these draws runs synchronously\n        // inside this effect, and a sync setState there trips the compiler\n        // lint. A microtask still resolves before the browser paints, so the\n        // reveal is not delayed by a frame.\n        if (!announcedPaint) {\n          announcedPaint = true\n          queueMicrotask(() => {\n            /*\n              Re-check: the context can be evicted between this draw and the\n              microtask, and mounting one more orb anywhere on the page is\n              enough to do it — opening the details drawer over a full gallery\n              is exactly that. Revealing on the strength of a frame that has\n              already been thrown away puts a dead canvas on screen, which is\n              what the browser draws its broken-canvas placeholder over. The\n              generation that follows the restore announces again.\n            */\n            if (gl.isContextLost()) return\n            setPaintedKey(variant.key)\n          })\n        }\n      }\n\n      const releaseGL = () => {\n        resizeObserver?.disconnect()\n        intersectionObserver?.disconnect()\n        gl.deleteProgram(prog)\n        gl.deleteShader(vs)\n        gl.deleteShader(fs)\n        gl.deleteBuffer(buf)\n      }\n\n      if (reduceMotion) {\n        // One representative frame, then stop — snapped straight onto the\n        // state's targets, since a spring would only be part-way there.\n        tSec = 1\n        uploadAndDraw(1, true)\n        return releaseGL\n      }\n\n      const loop = () => {\n        raf = requestAnimationFrame(loop)\n        const now = performance.now() / 1000\n        const dt = Math.min(now - last, 0.05)\n        last = now\n        if (!visible || pausedRef.current) return\n        tSec += dt\n\n        /*\n          Adaptive resolution. When the smoothed frame time sits above ~30fps,\n          the GPU is drowning in fragment work (these shaders are pure fill\n          cost), so step the backing store down 20% and re-measure. Steps only\n          go down — never back up — so the resolution can't oscillate. The\n          warm-up guard keeps page-load jank (hydration, first compiles) from\n          triggering a downgrade the GPU never asked for.\n        */\n        /*\n          Only unstalled frames are evidence about GPU fill cost. `dt` above is\n          clamped at 0.05, so a frame that hits the clamp is the main thread\n          having been blocked — a slider drag re-rendering React, a GC pause, a\n          tab regaining focus — and feeding those in made UI jank look\n          identical to a drowning GPU.\n        */\n        if (dt < 0.05) frameEma += (dt - frameEma) * 0.08\n\n        if (tSec > 1.5 && frameEma > 1 / 34 && resScale > 0.5) {\n          resScale = Math.max(0.5, resScale * 0.8)\n          frameEma = 1 / 60 // require fresh evidence before the next step\n          resize()\n        } else if (tSec > 1.5 && frameEma < 1 / 55 && resScale < 1) {\n          /*\n            And step back up once frames are comfortably fast again. This used\n            to be one-way, on the reasoning that it could not then oscillate —\n            but that also meant one transient stall permanently halved the\n            orb's resolution, and at half resolution a high-frequency shader\n            aliases into shimmer that reads as the shader itself misbehaving.\n            The gap between the two thresholds (29ms down, 18ms up) is the\n            hysteresis that stops it hunting.\n          */\n          resScale = Math.min(1, resScale / 0.8)\n          frameEma = 1 / 60\n          resize()\n        }\n\n        uploadAndDraw(dt)\n      }\n      /*\n        First frame synchronously, before entering the rAF loop. rAF does not\n        run at all in hidden documents (background tabs, embedded previews),\n        so a freshly mounted orb would otherwise sit fully transparent until\n        the page next becomes visible — a grid of mounted, healthy, blank\n        canvases. The sync frame guarantees every mount paints: background\n        documents get a static frame, visible ones start animating over it.\n        dt = 1 lands the param glide on its targets, as in the reduce-motion\n        frame above.\n      */\n      uploadAndDraw(1)\n      loop()\n\n      return () => {\n        cancelAnimationFrame(raf)\n        releaseGL()\n      }\n    }\n\n    /*\n      Wire the canvas's lifecycle controller. The listeners are attached ONCE\n      per canvas element and never removed, deliberately: the lost event must\n      be canceled even while no orb is mounted on the canvas — an uncanceled\n      webglcontextlost marks the context permanently unrestorable, and the\n      router can show this exact canvas again later. Whether a loss leads to\n      a revival is decided by `desired`, not by listener presence.\n    */\n    /*\n      A canvas whose context has gone is not simply blank: Chrome paints its\n      broken-image placeholder over the element's whole box — the white square\n      you see on a reloaded grid. Hide the canvas the moment the context is\n      lost, and let the generation that follows a restore reveal it again.\n    */\n    const hideNow = () => {\n      /*\n        Both, deliberately. The style write lands in this tick — the browser\n        paints its placeholder over a dead canvas immediately, and a busy main\n        thread can hold a React update for several frames. The state change is\n        what keeps React's own view in sync, so the reveal that follows a\n        restore clears the inline value again rather than fighting it.\n      */\n      canvas.style.opacity = \"0\"\n      setPaintedKey(null)\n    }\n\n    const onContextLostHide = () => hideNow()\n    canvas.addEventListener(\"webglcontextlost\", onContextLostHide)\n\n    /*\n      Hand the context back before the next document asks for one.\n\n      A hard reload never runs this effect's cleanup — the document is\n      discarded whole — so the outgoing page's contexts are still alive while\n      the incoming page allocates its own. On a grid of orbs that puts the\n      live count past the browser's ~16 cap, and the ones it evicts are\n      exactly the canvases that come back as placeholders until the restore\n      path catches them. `persisted` is a bfcache suspend, where the page is\n      shown again untouched and must keep everything it holds.\n    */\n    const onPageHide = (event: PageTransitionEvent) => {\n      if (event.persisted) return\n      try {\n        loseExt?.loseContext()\n      } catch {\n        // Already released — nothing to hand back.\n      }\n    }\n    window.addEventListener(\"pagehide\", onPageHide)\n\n    let ctl = canvasControllers.get(canvas)\n    if (!ctl) {\n      const created: CanvasContextController = {\n        desired: false,\n        start: null,\n        stopGen: null,\n      }\n      canvas.addEventListener(\"webglcontextlost\", (event) => {\n        event.preventDefault() // always cancel — keeps the context restorable\n        created.stopGen?.()\n        created.stopGen = null\n        if (created.desired) {\n          /*\n            Ask for the context back — but in a LATER task. The browser only\n            marks a loss as restorable once the lost event's dispatch has\n            completed and it has seen the canceled flag, so a restoreContext()\n            issued during dispatch (or before it, as the mount path may) is\n            silently refused. This is the path a synchronous cleanup+setup\n            pair hits — React re-running the effect on the same canvas loses\n            the context and wants it right back. For losses we didn't cause\n            (eviction, GPU reset) the call may refuse; the canceled event then\n            lets the browser restore on its own schedule.\n          */\n          setTimeout(() => {\n            if (!created.desired) return\n            try {\n              loseExt?.restoreContext()\n            } catch {\n              // Natural loss — restoration is the browser's call now.\n            }\n          }, 0)\n        }\n      })\n      canvas.addEventListener(\"webglcontextrestored\", () => {\n        if (created.desired && created.start) {\n          created.stopGen = created.start()\n        }\n      })\n      canvasControllers.set(canvas, created)\n      ctl = created\n    }\n    const controller = ctl\n\n    controller.desired = true\n    controller.start = startGeneration\n    if (gl.isContextLost()) {\n      // A previous run on this canvas released the context (effect re-run, or\n      // the router re-showing a kept-alive page). If the lost event already\n      // dispatched this request is honored now; if it is still queued, the\n      // lost handler above re-requests it on dispatch.\n      try {\n        loseExt?.restoreContext()\n      } catch {\n        // No restore path — the orb stays blank rather than throwing.\n      }\n    } else {\n      controller.stopGen = startGeneration()\n    }\n\n    return () => {\n      /*\n        Hide BEFORE tearing the context down.\n\n        This cleanup releases the context deliberately, and the lost event it\n        provokes is dispatched asynchronously — by which point the listener\n        below is unhooked and the replacement effect has not drawn yet. That\n        leaves a revealed canvas with a dead context, which is precisely what\n        the browser paints its broken-canvas placeholder over. The window is\n        not rare: any prop change in this effect's deps re-runs it, so it hits\n        every time the drawer switches preview example (maxDpr differs on the\n        layout one) or a wrapper is toggled.\n      */\n      hideNow()\n      canvas.removeEventListener(\"webglcontextlost\", onContextLostHide)\n      window.removeEventListener(\"pagehide\", onPageHide)\n      controller.desired = false\n      controller.start = null\n      controller.stopGen?.()\n      controller.stopGen = null\n      /*\n        Release the context NOW instead of when the canvas is garbage\n        collected. Browsers cap live WebGL contexts per page (~8–16) and evict\n        the oldest when the cap is hit — client-side navigation that unmounts\n        and remounts a page of orbs otherwise piles up zombie contexts until\n        freshly mounted orbs get evicted and render blank.\n      */\n      try {\n        loseExt?.loseContext()\n      } catch {\n        // Context already lost — nothing to release.\n      }\n    }\n    /*\n      `wrapped` is in here because turning a wrapper on or off moves the canvas\n      from being this component's root element to being a child of the wrapper\n      div — React drops the old element and mounts a new one, and the GL\n      context, its observers and its render loop all belong to the old one. Any\n      other prop leaves the element alone, INCLUDING a swap between two\n      wrappers: the canvas keeps its slot among the decoration layers, so\n      glass -> ring reuses the context instead of rebuilding it.\n    */\n  }, [variant, pauseOffscreen, maxDpr, wrapped])\n\n  const sizeStyle: CSSProperties =\n    size === undefined ? {} : { width: size, height: size }\n\n  /*\n    Spread ahead of the caller's `style`, so an orb that wants to own its own\n    opacity still can — it simply opts out of the reveal.\n\n    A hard flip, deliberately: no transition, no fade. A hidden document\n    (background tab, embedded preview) does not advance CSS transitions, so a\n    faded reveal left orbs pinned at zero in exactly the case the synchronous\n    first frame above exists to serve — mounted, healthy, and invisible. The\n    cut is not a pop either way, since it happens on the frame the orb first\n    has something to show.\n  */\n  const revealStyle: CSSProperties = painted ? {} : { opacity: 0 }\n\n  const canvas = (\n    <canvas\n      // A lost WebGL context can't be reused, so each variant gets a fresh canvas.\n      key={variant.key}\n      ref={canvasRef}\n      className={spec ? undefined : className}\n      style={\n        spec\n          ? {\n              display: \"block\",\n              position: \"absolute\",\n              left: `${spec.inset}%`,\n              top: `${spec.inset}%`,\n              width: `${100 - 2 * spec.inset}%`,\n              height: `${100 - 2 * spec.inset}%`,\n              ...revealStyle,\n              ...(spec.mask ? masked(spec.mask) : {}),\n            }\n          : { display: \"block\", ...sizeStyle, ...revealStyle, ...style }\n      }\n      role={!spec && ariaLabel ? \"img\" : undefined}\n      aria-label={spec ? undefined : ariaLabel}\n      aria-hidden={!spec && ariaLabel ? undefined : true}\n    />\n  )\n\n  if (!spec) return canvas\n\n  /*\n    Wrapped: the box becomes the orb's footprint and the canvas is absolutely\n    positioned inside it. `under`, the canvas and `over` are all positioned\n    with an auto z-index, so they paint in DOM order — decoration behind the\n    orb, then the orb, then decoration in front of it.\n\n    `aspectRatio` is the fallback for the sizeless case: `size` is optional\n    (callers may size the orb with a class instead), and an absolutely\n    positioned canvas contributes nothing to its parent's height, so without\n    it a class that only sets a width would collapse the box to zero.\n  */\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"relative\",\n        aspectRatio: \"1 / 1\",\n        ...(spec.shadow ? { borderRadius: \"50%\", boxShadow: spec.shadow } : {}),\n        ...sizeStyle,\n        ...(wrapperColor ? { color: wrapperColor } : {}),\n        ...style,\n      }}\n      role={ariaLabel ? \"img\" : undefined}\n      aria-label={ariaLabel}\n      aria-hidden={ariaLabel ? undefined : true}\n    >\n      {spec.animated ? (\n        <style\n          href={WRAPPER_STYLE_HREF}\n          precedence=\"default\"\n          dangerouslySetInnerHTML={{ __html: WRAPPER_CSS }}\n        />\n      ) : null}\n      {spec.under}\n      {canvas}\n      {spec.over}\n    </div>\n  )\n}\n"
    },
    {
      "path": "components/examples/research-workflow.ts",
      "target": "@components/examples/research-workflow.ts",
      "type": "registry:component",
      "content": "export interface ResearchNote {\n  id: string\n  title: string\n  text: string\n  attached?: boolean\n}\nexport const researchProjects = [\n  {\n    id: \"field-notes\",\n    title: \"West dock interviews\",\n    subtitle: \"Field research · 5 interviews\",\n    question: \"What is slowing down the handoff?\",\n    notes: [\n      {\n        id: \"interview\",\n        title: \"Operator interviews\",\n        text: \"Four of five operators enter the same shipment identifier twice: first on paper, then on a shared tablet. The tablet often arrives after the paper sheet.\",\n      },\n      {\n        id: \"shift\",\n        title: \"Shift-change observation\",\n        text: \"At both observed shift changes, operators checked the clipboard and tablet before confirming which shipment record was current.\",\n      },\n      {\n        id: \"map\",\n        title: \"Workflow map\",\n        text: \"Paper, radio, and tablet steps each copy the shipment identifier. No step owns the canonical record. The team proposed confirming one record at intake and showing its sync status.\",\n      },\n    ],\n  },\n  {\n    id: \"accessibility\",\n    title: \"Wayfinding audit\",\n    subtitle: \"Station arrival · 2 observations\",\n    question: \"What should change in the station signs?\",\n    notes: [\n      {\n        id: \"arrival\",\n        title: \"Arrival walk-through\",\n        text: \"Three visitors paused before the platform turn. Transfer signs compete with advertising at the decision point, and the destination label changes between signs.\",\n      },\n      {\n        id: \"signs\",\n        title: \"Signage review\",\n        text: \"Repeat the same destination label at eye level before each turn. Keep arrows together with the label and validate the route with first-time visitors.\",\n      },\n    ],\n  },\n  {\n    id: \"handoff\",\n    title: \"Research handoff\",\n    subtitle: \"Product team · review brief\",\n    question: \"What needs to be in the handoff?\",\n    notes: [\n      {\n        id: \"review\",\n        title: \"Team review\",\n        text: \"The product team needs observed behavior, interpretation, and open questions separated. Every finding should include a field-note reference and a named owner for validation.\",\n      },\n      {\n        id: \"next\",\n        title: \"Next research round\",\n        text: \"Test the intake confirmation with two operators during one shift. Compare duplicate-entry frequency and ask each operator to identify the current shipment record.\",\n      },\n    ],\n  },\n]\n\n/** Extractive local demo: return the selected notes, never invented research. */\nexport function researchAnswer(\n  question: string,\n  notes: ResearchNote[],\n  concise = false\n) {\n  if (!notes.length)\n    return \"This conversation has no source notes yet. Attach a text note or open one of the research projects to ask about its evidence.\"\n  const terms =\n    question\n      .toLowerCase()\n      .match(/[a-z]{4,}/g)\n      ?.filter(\n        (word) =>\n          ![\n            \"what\",\n            \"with\",\n            \"from\",\n            \"that\",\n            \"this\",\n            \"about\",\n            \"should\",\n            \"could\",\n            \"please\",\n          ].includes(word)\n      ) ?? []\n  const ranked = notes\n    .map((note, index) => ({\n      note,\n      index,\n      score: terms.filter((term) =>\n        `${note.title} ${note.text}`.toLowerCase().includes(term)\n      ).length,\n    }))\n    .sort((a, b) => b.score - a.score)\n  const relevant =\n    ranked[0].score > 0 ? ranked.filter((item) => item.score > 0) : ranked\n  const chosen = relevant.slice(0, concise ? 1 : 3)\n  return `From ${chosen.length === 1 ? \"the selected note\" : \"the selected notes\"}:\\n\\n${chosen.map(({ note, index }) => `${note.text.slice(0, concise ? 220 : 480)}${note.text.length > (concise ? 220 : 480) ? \"…\" : \"\"} [${index + 1}]`).join(\"\\n\\n\")}`\n}\n\nexport function researchReport(\n  title: string,\n  notes: ResearchNote[],\n  answer: string\n) {\n  return `${title}\\n\\nRESEARCH BRIEF\\n\\n${answer}\\n\\nSOURCE NOTES\\n${notes.map((note, index) => `${index + 1}. ${note.title}\\n${note.text}`).join(\"\\n\\n\")}\\n\\nReview these observations with the project team before taking action.`\n}\n"
    },
    {
      "path": "styles/agents.css",
      "target": "~/styles/agents.css",
      "type": "registry:file",
      "content": "@theme inline {\n  --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;\n  --font-mono: var(--font-jetbrains-mono), ui-monospace, \"SF Mono\", monospace;\n  --radius-chip: 6px;\n  --radius-control: 8px;\n  --radius-card: 10px;\n  --radius-window: 14px;\n}\n\nhtml:root {\n  --color-background-full: oklch(0.985 0.001 286.376);\n  --color-background-primary-default: oklch(1 0 0);\n  --color-background-secondary-default: oklch(0.961 0.002 247.84);\n  --color-background-primary-hover: oklch(0.97 0.002 247.839);\n  --color-text-primary: oklch(0.247 0.006 258.361);\n  --color-text-secondary: oklch(0.506 0.01 264.477);\n  --color-text-tertiary: oklch(0.54 0.01 264.484);\n  --color-border-button-default: oklch(0.912 0.005 258.326);\n  --color-separator-border: oklch(0.946 0.003 264.542);\n  --font-mono-source: var(--font-jetbrains-mono);\n}\n\nhtml.dark {\n  --color-background-full: oklch(0.209 0.004 264.477);\n  --color-background-primary-default: oklch(0.26 0.006 271.191);\n  --color-background-secondary-default: oklch(0.231 0.004 264.487);\n  --color-background-primary-hover: oklch(0.289 0.006 271.22);\n  --color-text-primary: oklch(0.964 0.002 247.839);\n  --color-text-secondary: oklch(0.731 0.008 260.731);\n  --color-text-tertiary: oklch(0.7 0.008 260.731);\n  --color-border-button-default: oklch(0.356 0.007 264.474);\n  --color-separator-border: oklch(0.308 0.006 258.354);\n}\n\nhtml:root,\nhtml.dark {\n  --color-border-focus-ring: var(--color-text-primary);\n  --color-tab-count-selected-background: var(--color-background-secondary-default);\n  --background: var(--color-background-full);\n  --foreground: var(--color-text-primary);\n  --card: var(--color-background-primary-default);\n  --card-foreground: var(--color-text-primary);\n  --popover: var(--color-background-primary-default);\n  --popover-foreground: var(--color-text-primary);\n  --primary: var(--color-text-primary);\n  --primary-foreground: var(--color-background-primary-default);\n  --secondary: var(--color-background-secondary-default);\n  --secondary-foreground: var(--color-text-primary);\n  --muted: var(--color-background-secondary-default);\n  --muted-foreground: var(--color-text-secondary);\n  --accent: var(--color-background-primary-hover);\n  --accent-foreground: var(--color-text-primary);\n  --border: var(--color-separator-border);\n  --input: var(--color-border-button-default);\n  --sidebar: var(--color-background-full);\n  --sidebar-foreground: var(--color-text-primary);\n  --sidebar-accent: var(--color-background-secondary-default);\n  --sidebar-border: var(--color-separator-border);\n  --radius: 10px;\n}\n\nbody {\n  font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;\n  font-size: 14px;\n  line-height: 1.5;\n  letter-spacing: -0.01em;\n  font-feature-settings: \"cv11\", \"ss01\";\n  text-rendering: optimizeLegibility;\n}\n\n@media (max-width: 639px) {\n  input,\n  select,\n  textarea {\n    font-size: 16px;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  *,\n  *::before,\n  *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0.01ms !important;\n    scroll-behavior: auto !important;\n  }\n}\n\n/* Result layouts respond to the card, including compact grid and expanded views. */\n.generated-result {\n  container-type: inline-size;\n  container-name: result;\n}\n@container result (min-width: 440px) {\n  .result-comparison-grid {\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n  }\n}\n"
    },
    {
      "path": "components/beautiful-ui/LICENSE",
      "target": "@components/beautiful-ui/LICENSE",
      "type": "registry:file",
      "content": "MIT License\n\nCopyright (c) 2026 Shane Levine\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
    },
    {
      "path": "components/boardui/LICENSE",
      "target": "@components/boardui/LICENSE",
      "type": "registry:file",
      "content": "MIT License\n\nCopyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
    },
    {
      "path": "components/boardui/styles/globals.css",
      "target": "@components/boardui/styles/globals.css",
      "type": "registry:file",
      "content": "@import \"tailwindcss\";\n@import \"./theme.css\";\n@import \"./typography.css\";\n\n/* ---------------------------------------------------------------------------\n * Dark mode strategy\n *\n * We use a class-based dark variant. Toggle dark mode by adding the `.dark`\n * class to <html> (a theme provider like `next-themes` can do this later).\n *\n *   <html class=\"dark\">  →  every `dark:*` utility activates\n *\n * Components should NOT use `dark:` prefixes directly. Instead they reference\n * semantic color tokens (e.g. `bg-bg-primary`, `text-text-primary`) that flip\n * under `.dark` in styles/theme.css.\n * --------------------------------------------------------------------------- */\n@custom-variant dark (&:where(.dark, .dark *));\n\n/* The next theme is revealed by ThemeToggle from the exact interaction point.\n * Native view-transition crossfades are disabled so only the soft circular\n * clip-path animation runs; unsupported browsers simply ignore these rules. */\n::view-transition-old(root),\n::view-transition-new(root) {\n  animation: none;\n  mix-blend-mode: normal;\n}\n\n@keyframes ai-chat-text-shimmer {\n  from {\n    background-position: 200% center;\n  }\n\n  to {\n    background-position: -100% center;\n  }\n}\n\n.agent-progress-loading-text {\n  color: transparent;\n  background-image: linear-gradient(\n    100deg,\n    var(--color-text-secondary) 16%,\n    var(--color-text-secondary) 38%,\n    var(--color-text-primary) 50%,\n    var(--color-text-secondary) 62%,\n    var(--color-text-secondary) 84%\n  );\n  background-position: 200% center;\n  background-size: 300% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  animation: ai-chat-text-shimmer 3.4s linear infinite;\n  will-change: background-position;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .agent-progress-loading-text {\n    color: var(--color-text-secondary);\n    background-image: none;\n    animation: none;\n  }\n}\n\n::view-transition-old(root) {\n  z-index: 1;\n}\n\n::view-transition-new(root) {\n  z-index: 2;\n}\n\n/* Components often have their own hover/focus color transitions. Freeze them\n * while the browser captures the next theme so the expanding circle contains\n * the fully resolved new palette instead of a frame that is still fading from\n * the old one. This is what makes the click-origin reveal read clearly. */\n.theme-transitioning,\n.theme-transitioning * {\n  transition: none !important;\n}\n\n/* Reset + base */\nhtml,\nbody {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  font-variant-ligatures: contextual;\n  font-kerning: normal;\n}\n\nhtml {\n  color-scheme: light;\n  background-color: var(--color-background-full);\n}\nhtml.dark {\n  color-scheme: dark;\n}\nbody {\n  background-color: var(--color-background-full);\n  color: var(--color-text-primary);\n}\n\n/* Theme-aware monochrome BoardUI mark. Both images keep their intrinsic\n * dimensions while the root theme class decides which one participates in\n * layout, avoiding a light-logo flash during hydration. */\n.theme-logo-dark,\n.theme-asset-dark {\n  display: none;\n}\n.dark .theme-logo-light,\n.dark .theme-asset-light {\n  display: none;\n}\n.dark .theme-logo-dark,\n.dark .theme-asset-dark {\n  display: block;\n}\n\n/* Tailwind expands shadow utilities into `--tw-shadow` at build time. Point\n * those generated utilities back to the live dark elevation tokens so the\n * class-based theme can adjust opacity without component-level overrides. */\n.dark .shadow-2xs {\n  --tw-shadow: var(--shadow-2xs);\n}\n.dark .shadow-xs {\n  --tw-shadow: var(--shadow-xs);\n}\n.dark .shadow-sm {\n  --tw-shadow: var(--shadow-sm);\n}\n.dark .shadow-md {\n  --tw-shadow: var(--shadow-md);\n}\n.dark .shadow-lg {\n  --tw-shadow: var(--shadow-lg);\n}\n.dark .shadow-xl {\n  --tw-shadow: var(--shadow-xl);\n}\n.dark .shadow-card {\n  --tw-shadow: var(--shadow-card);\n}\n.dark .shadow-dropdown {\n  --tw-shadow: var(--shadow-dropdown);\n}\n.dark .shadow-sidebar {\n  --tw-shadow: var(--shadow-sidebar);\n}\n.dark .shadow-waitlist {\n  --tw-shadow: var(--shadow-waitlist);\n}\n\n/* Hide default expand arrow on Safari */\ndetails summary::-webkit-details-marker {\n  display: none;\n}\n\n/* Hide default number-input spinners */\ninput::-webkit-outer-spin-button,\ninput::-webkit-inner-spin-button {\n  -webkit-appearance: none;\n  margin: 0;\n}\ninput[type=\"number\"] {\n  -moz-appearance: textfield;\n}\n\n/* Hide default search clear button */\ninput[type=\"search\"]::-webkit-search-cancel-button {\n  -webkit-appearance: none;\n}\n\n/* iOS Safari auto-zooms the page when a focused form field's font-size is\n * under 16px — our text-body-regular/medium tokens are 14px by design, so\n * bump just the font-size (not the whole token) on small screens instead of\n * changing the design system's type scale. */\n@media (max-width: 639px) {\n  input,\n  select,\n  textarea {\n    font-size: 16px;\n  }\n}\n\n/* Soft fade applied to an animated number each time its target changes */\n@keyframes number-fade {\n  from {\n    opacity: 0.35;\n  }\n  to {\n    opacity: 1;\n  }\n}\n.animate-number-fade {\n  animation: number-fade 220ms ease-out;\n}\n\n/* Recharts' accessibility layer makes the chart surface and its shapes\n * (bars, sectors, dots) keyboard-focusable, and Chrome paints the UA focus\n * outline around whichever one you click - a blue box around a ring segment\n * or a bar. Every chart card drives its own focus feedback (hover darkening,\n * headline swap), so the outline is noise: hide it across every Recharts\n * chart. Keyboard users still get the cards' own active states. */\n.recharts-wrapper:focus,\n.recharts-wrapper:focus-visible,\n.recharts-wrapper :focus,\n.recharts-wrapper :focus-visible {\n  outline: none;\n}\n\n/* Liquid glass (landing nav + hero) — the rim light.\n *\n * A fixed conic gradient standing in for a bevel lit from a single direction:\n * bright along the top edge where the light source is, dark down both sides\n * where the glass turns away, and a weaker bounce along the bottom. It does\n * not rotate — a travelling highlight reads as an animation playing on the\n * control, where a real edge just sits there catching the room.\n *\n * Achromatic on purpose: highlights are white, shading is black, nothing in\n * between carries a hue.\n *\n * The two-layer mask (padding-box XOR full box) keeps only the 1.5px ring,\n * letting the gradient paint the stroke without covering the glass. */\n\n.landing-glass-stroke {\n  pointer-events: none;\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  /* Every value here is set inline by `LiquidGlassSurface` from the shared\n   * glass config; the fallbacks are only for a bare use of the class. */\n  padding: var(--lg-rim-width, 1.5px);\n  background: conic-gradient(\n    from var(--lg-rim-angle, 0deg),\n    var(--lg-rim-hi, rgb(255 255 255 / 0.9)) 0deg,\n    var(--lg-rim-sh, rgb(0 0 0 / 0.14)) 70deg,\n    var(--lg-rim-dim, rgb(255 255 255 / 0.1)) 140deg,\n    var(--lg-rim-bounce, rgb(255 255 255 / 0.55)) 180deg,\n    var(--lg-rim-dim, rgb(255 255 255 / 0.1)) 220deg,\n    var(--lg-rim-sh, rgb(0 0 0 / 0.14)) 290deg,\n    var(--lg-rim-hi, rgb(255 255 255 / 0.9)) 360deg\n  );\n  /* Shorthands first, composites last: `-webkit-mask` aliases `mask` in\n   * Chromium, so a shorthand placed after a composite silently resets it to\n   * `add` — and an un-composited mask paints the full conic wash over the\n   * button instead of a ring. */\n  -webkit-mask:\n    linear-gradient(#000 0 0) content-box,\n    linear-gradient(#000 0 0);\n  mask:\n    linear-gradient(#000 0 0) content-box,\n    linear-gradient(#000 0 0);\n  -webkit-mask-composite: xor;\n  mask-composite: exclude;\n  opacity: var(--lg-rim-o, 1);\n  transition: opacity var(--lg-ms, 200ms) ease;\n}\n\n/* Liquid glass hover.\n *\n * Resting and hover values arrive together as inline custom properties, and\n * the swap happens here in CSS rather than in React state — hover then costs\n * a style recalculation instead of a re-render of everything inside the glass.\n * `.lg-hover` is the opt-in: a surface without it keeps its resting value. */\n.lg-tint {\n  background-color: var(--lg-tint, transparent);\n  transition: background-color var(--lg-ms, 200ms) ease;\n}\n\n.lg-sheen {\n  opacity: var(--lg-sheen, 0.5);\n  transition: opacity var(--lg-ms, 200ms) ease;\n}\n\n.lg-host {\n  box-shadow: var(--lg-shadow, none);\n  transition:\n    box-shadow var(--lg-ms, 200ms) ease,\n    transform var(--lg-ms, 200ms) ease;\n}\n\n.lg-host:hover {\n  box-shadow: var(--lg-shadow-hover, var(--lg-shadow, none));\n  transform: translateY(var(--lg-lift, 0px)) scale(var(--lg-scale, 1));\n}\n\n.group:hover .lg-tint.lg-hover {\n  background-color: var(--lg-tint-hover, var(--lg-tint, transparent));\n}\n\n.group:hover .lg-sheen.lg-hover {\n  opacity: var(--lg-sheen-hover, var(--lg-sheen, 0.5));\n}\n\n.group:hover .landing-glass-stroke.lg-hover {\n  opacity: var(--lg-rim-o-hover, var(--lg-rim-o, 1));\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .lg-tint,\n  .lg-sheen,\n  .lg-host,\n  .landing-glass-stroke {\n    transition: none;\n  }\n\n  .lg-host:hover {\n    transform: none;\n  }\n}\n\n/* Split-flap (\"Solari\" board) digit, used by the image-generation countdown.\n * Two leaves hinged on the card's centre seam:\n *   flap-fall  the outgoing digit's TOP half swings down over the incoming\n *              bottom half. Eased IN, because a real flap is falling under\n *              gravity — it accelerates — and it darkens as it tips away\n *              from the light.\n *   flap-rise  the incoming digit's BOTTOM half swings up from edge-on,\n *              starting exactly when the fall lands (delay = fall duration,\n *              `both` so it waits at 90deg instead of flashing flat). The\n *              small overshoot past 0deg is the slap against the stop —\n *              that bounce is what makes it read mechanical rather than\n *              like a plain rotation. */\n@keyframes flap-fall {\n  from {\n    transform: rotateX(0deg);\n    filter: brightness(1);\n  }\n  to {\n    transform: rotateX(-90deg);\n    filter: brightness(0.86);\n  }\n}\n@keyframes flap-rise {\n  0% {\n    transform: rotateX(90deg);\n    filter: brightness(0.86);\n  }\n  70% {\n    transform: rotateX(-9deg);\n  }\n  86% {\n    transform: rotateX(4deg);\n  }\n  100% {\n    transform: rotateX(0deg);\n    filter: brightness(1);\n  }\n}\n.animate-flap-fall {\n  transform-origin: bottom center;\n  backface-visibility: hidden;\n  animation: flap-fall 150ms cubic-bezier(0.55, 0, 0.9, 0.35) forwards;\n}\n.animate-flap-rise {\n  transform-origin: top center;\n  backface-visibility: hidden;\n  animation: flap-rise 230ms cubic-bezier(0.25, 0.7, 0.35, 1) 150ms both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .animate-flap-fall,\n  .animate-flap-rise {\n    animation-duration: 1ms;\n    animation-delay: 0ms;\n  }\n}\n\n/* New-row entrance for the Storage settings table. Three coordinated\n * pieces (wrapper pairs with `grid grid-rows-[1fr]` + an overflow-hidden\n * middle layer, content carries the slide):\n *   row-grow-in   wrapper's grid track 0fr → 1fr — pushes the rows below\n *                 down smoothly.\n *   row-slide-in  content translates -100% → 0 with the SAME duration and\n *                 easing, so it stays glued to the growing wrapper's bottom\n *                 edge — visually the row slides out from underneath the\n *                 header row above it.\n *   row-fade-in   opacity/blur on a SHORTER clock than the movement — the\n *                 row is fully visible before it finishes settling (fades\n *                 must complete before movement ends). */\n@keyframes row-grow-in {\n  from {\n    grid-template-rows: 0fr;\n  }\n  to {\n    grid-template-rows: 1fr;\n  }\n}\n@keyframes row-slide-in {\n  from {\n    translate: 0 -100%;\n  }\n}\n@keyframes row-fade-in {\n  from {\n    opacity: 0;\n    filter: blur(2px);\n  }\n}\n.animate-row-grow-in {\n  animation: row-grow-in 400ms ease-out;\n}\n.animate-row-slide-in {\n  animation:\n    row-slide-in 400ms ease-out,\n    row-fade-in 240ms ease-out;\n}\n\n/* Reverse of the Storage row entrance. The content rises into the clipped\n * top edge while its grid track collapses, so the following rows move upward\n * continuously and the deleted row disappears beneath the header surface. */\n@keyframes row-collapse-out {\n  from {\n    grid-template-rows: 1fr;\n  }\n  to {\n    grid-template-rows: 0fr;\n  }\n}\n@keyframes row-slide-out {\n  to {\n    translate: 0 -100%;\n  }\n}\n@keyframes row-fade-out {\n  to {\n    opacity: 0;\n    filter: blur(2px);\n  }\n}\n.animate-row-collapse-out {\n  animation: row-collapse-out 225ms ease-in forwards;\n}\n.animate-row-slide-out {\n  animation:\n    row-slide-out 225ms ease-in forwards,\n    row-fade-out 160ms ease-in forwards;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .animate-row-grow-in,\n  .animate-row-slide-in,\n  .animate-row-collapse-out,\n  .animate-row-slide-out {\n    animation-duration: 1ms;\n  }\n}\n\n/* Transitions.dev — \"Texts reveal\", used by the Storage dropzone's content\n * swaps (idle copy ↔ file name + status line).\n *\n * Entrance (`.is-shown` on the parent): lines rise 12px from below while\n * un-blurring, 500ms on a soft-landing bezier, staggered 40ms per line so\n * the eye lands on the first line first.\n * Exit (`.is-hiding`): a single quiet 200ms opacity fade in place — no Y\n * return, no blur, no stagger — so the disappearance never reads as a\n * reverse reveal.\n *\n * Deviations from the original snippet: custom props are scoped to\n * `.t-stagger` instead of :root, and `.t-stagger-line` doesn't force\n * `display: block` so flex-based lines (the icon disc) keep their layout. */\n.t-stagger {\n  --stagger-dur: 500ms;\n  --stagger-distance: 12px;\n  --stagger-stagger: 40ms;\n  --stagger-blur: 3px;\n  --stagger-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n.t-stagger-line {\n  opacity: 0;\n  transform: translateY(var(--stagger-distance));\n  filter: blur(var(--stagger-blur));\n  transition:\n    opacity var(--stagger-dur) var(--stagger-ease),\n    transform var(--stagger-dur) var(--stagger-ease),\n    filter var(--stagger-dur) var(--stagger-ease);\n  will-change: transform, opacity, filter;\n}\n.t-stagger-line--2 {\n  transition-delay: var(--stagger-stagger);\n}\n.t-stagger-line--3 {\n  transition-delay: calc(var(--stagger-stagger) * 2);\n}\n.t-stagger.is-shown .t-stagger-line {\n  opacity: 1;\n  transform: translateY(0);\n  filter: blur(0);\n}\n.t-stagger.is-hiding .t-stagger-line {\n  opacity: 0;\n  transform: translateY(0);\n  filter: blur(0);\n  transition:\n    opacity 200ms ease,\n    transform 0s linear,\n    filter 0s linear;\n  transition-delay: 0s;\n}\n@media (prefers-reduced-motion: reduce) {\n  .t-stagger-line {\n    transition: none !important;\n  }\n}\n\n/* Rolling-counter label swap: the old value lifts 5px and fades out while the\n * new one rises in from 5px below and fades in. */\n@keyframes label-out {\n  from {\n    transform: translateY(0);\n    opacity: 1;\n  }\n  to {\n    transform: translateY(-10px);\n    opacity: 0;\n  }\n}\n@keyframes label-in {\n  from {\n    transform: translateY(10px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n.animate-label-out {\n  animation: label-out 220ms ease-out forwards;\n}\n.animate-label-in {\n  animation: label-in 220ms ease-out;\n}\n\n/* Checkbox tick: draws the stroke on like it's being ticked by hand.\n * The path sets pathLength=1 so the dash math is resolution-independent. */\n@keyframes check-draw {\n  from {\n    stroke-dashoffset: 1;\n  }\n  to {\n    stroke-dashoffset: 0;\n  }\n}\n.animate-check-draw {\n  stroke-dasharray: 1;\n  stroke-dashoffset: 1;\n  animation: check-draw 200ms cubic-bezier(0.65, 0, 0.35, 1) forwards;\n}\n@media (prefers-reduced-motion: reduce) {\n  .animate-check-draw {\n    animation: none;\n    stroke-dashoffset: 0;\n  }\n}\n\n/* ---------------------------------------------------------------------------\n * Chart entrance animations (AI profile template & contributions heatmap)\n * ------------------------------------------------------------------------- */\n\n/* Heatmap cells pop in softly — each cell gets a random-looking per-cell\n * animation-delay so the grid fills in a scattered order. */\n.contributions-grid {\n  --contribution-tier-0: var(--color-chart-neutral);\n}\n.contributions-grid[data-accent=\"emerald\"] {\n  --contribution-tier-1: var(--color-emerald-200); --contribution-tier-2: var(--color-emerald-400); --contribution-tier-3: var(--color-emerald-500); --contribution-tier-4: var(--color-emerald-600); --contribution-tier-5: var(--color-emerald-700);\n}\n.contributions-grid[data-accent=\"green\"] {\n  --contribution-tier-1: var(--color-green-200); --contribution-tier-2: var(--color-green-400); --contribution-tier-3: var(--color-green-500); --contribution-tier-4: var(--color-green-600); --contribution-tier-5: var(--color-green-700);\n}\n.contributions-grid[data-accent=\"teal\"] {\n  --contribution-tier-1: var(--color-teal-200); --contribution-tier-2: var(--color-teal-400); --contribution-tier-3: var(--color-teal-500); --contribution-tier-4: var(--color-teal-600); --contribution-tier-5: var(--color-teal-700);\n}\n.contributions-grid[data-accent=\"cyan\"] {\n  --contribution-tier-1: var(--color-cyan-200); --contribution-tier-2: var(--color-cyan-400); --contribution-tier-3: var(--color-cyan-500); --contribution-tier-4: var(--color-cyan-600); --contribution-tier-5: var(--color-cyan-700);\n}\n.contributions-grid[data-accent=\"blue\"] {\n  --contribution-tier-1: var(--color-blue-200); --contribution-tier-2: var(--color-blue-400); --contribution-tier-3: var(--color-blue-500); --contribution-tier-4: var(--color-blue-600); --contribution-tier-5: var(--color-blue-700);\n}\n.contributions-grid[data-accent=\"indigo\"] {\n  --contribution-tier-1: var(--color-indigo-200); --contribution-tier-2: var(--color-indigo-400); --contribution-tier-3: var(--color-indigo-500); --contribution-tier-4: var(--color-indigo-600); --contribution-tier-5: var(--color-indigo-700);\n}\n.contributions-grid[data-accent=\"violet\"] {\n  --contribution-tier-1: var(--color-violet-200); --contribution-tier-2: var(--color-violet-400); --contribution-tier-3: var(--color-violet-500); --contribution-tier-4: var(--color-violet-600); --contribution-tier-5: var(--color-violet-700);\n}\n.contributions-grid[data-accent=\"rose\"] {\n  --contribution-tier-1: var(--color-rose-200); --contribution-tier-2: var(--color-rose-400); --contribution-tier-3: var(--color-rose-500); --contribution-tier-4: var(--color-rose-600); --contribution-tier-5: var(--color-rose-700);\n}\n.contributions-grid[data-accent=\"amber\"] {\n  --contribution-tier-1: var(--color-amber-200); --contribution-tier-2: var(--color-amber-400); --contribution-tier-3: var(--color-amber-500); --contribution-tier-4: var(--color-amber-600); --contribution-tier-5: var(--color-amber-700);\n}\n\n.dark .contributions-grid[data-accent=\"emerald\"] {\n  --contribution-tier-1: var(--color-emerald-950); --contribution-tier-2: var(--color-emerald-800); --contribution-tier-3: var(--color-emerald-700); --contribution-tier-4: var(--color-emerald-600); --contribution-tier-5: var(--color-emerald-500);\n}\n.dark .contributions-grid[data-accent=\"green\"] {\n  --contribution-tier-1: var(--color-green-950); --contribution-tier-2: var(--color-green-800); --contribution-tier-3: var(--color-green-700); --contribution-tier-4: var(--color-green-600); --contribution-tier-5: var(--color-green-500);\n}\n.dark .contributions-grid[data-accent=\"teal\"] {\n  --contribution-tier-1: var(--color-teal-950); --contribution-tier-2: var(--color-teal-800); --contribution-tier-3: var(--color-teal-700); --contribution-tier-4: var(--color-teal-600); --contribution-tier-5: var(--color-teal-500);\n}\n.dark .contributions-grid[data-accent=\"cyan\"] {\n  --contribution-tier-1: var(--color-cyan-950); --contribution-tier-2: var(--color-cyan-800); --contribution-tier-3: var(--color-cyan-700); --contribution-tier-4: var(--color-cyan-600); --contribution-tier-5: var(--color-cyan-500);\n}\n.dark .contributions-grid[data-accent=\"blue\"] {\n  --contribution-tier-1: var(--color-blue-950); --contribution-tier-2: var(--color-blue-800); --contribution-tier-3: var(--color-blue-700); --contribution-tier-4: var(--color-blue-600); --contribution-tier-5: var(--color-blue-500);\n}\n.dark .contributions-grid[data-accent=\"indigo\"] {\n  --contribution-tier-1: var(--color-indigo-950); --contribution-tier-2: var(--color-indigo-800); --contribution-tier-3: var(--color-indigo-700); --contribution-tier-4: var(--color-indigo-600); --contribution-tier-5: var(--color-indigo-500);\n}\n.dark .contributions-grid[data-accent=\"violet\"] {\n  --contribution-tier-1: var(--color-violet-950); --contribution-tier-2: var(--color-violet-800); --contribution-tier-3: var(--color-violet-700); --contribution-tier-4: var(--color-violet-600); --contribution-tier-5: var(--color-violet-500);\n}\n.dark .contributions-grid[data-accent=\"rose\"] {\n  --contribution-tier-1: var(--color-rose-950); --contribution-tier-2: var(--color-rose-800); --contribution-tier-3: var(--color-rose-700); --contribution-tier-4: var(--color-rose-600); --contribution-tier-5: var(--color-rose-500);\n}\n.dark .contributions-grid[data-accent=\"amber\"] {\n  --contribution-tier-1: var(--color-amber-950); --contribution-tier-2: var(--color-amber-800); --contribution-tier-3: var(--color-amber-700); --contribution-tier-4: var(--color-amber-600); --contribution-tier-5: var(--color-amber-500);\n}\n\n.contribution-cell[data-tier=\"0\"] { background-color: var(--contribution-tier-0); }\n.contribution-cell[data-tier=\"1\"] { background-color: var(--contribution-tier-1); }\n.contribution-cell[data-tier=\"2\"] { background-color: var(--contribution-tier-2); }\n.contribution-cell[data-tier=\"3\"] { background-color: var(--contribution-tier-3); }\n.contribution-cell[data-tier=\"4\"] { background-color: var(--contribution-tier-4); }\n.contribution-cell[data-tier=\"5\"] { background-color: var(--contribution-tier-5); }\n\n@keyframes cell-pop {\n  from {\n    opacity: 0;\n    transform: scale(0.5);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n.animate-cell-pop {\n  animation: cell-pop 380ms ease-out both;\n}\n\n/* Bars grow up from the baseline; a per-bar delay staggers them left→right. */\n@keyframes bar-rise {\n  from {\n    transform: scaleY(0);\n  }\n  to {\n    transform: scaleY(1);\n  }\n}\n.animate-bar-rise {\n  transform-origin: bottom;\n  animation: bar-rise 360ms cubic-bezier(0.22, 1, 0.36, 1) both;\n}\n\n/* Line chart draws in left→right via a clip-path sweep over the whole plot\n * (line + gradient area together). */\n@keyframes chart-reveal {\n  from {\n    clip-path: inset(0 100% 0 0);\n  }\n  to {\n    clip-path: inset(0 0 0 0);\n  }\n}\n.animate-chart-reveal {\n  animation: chart-reveal 1800ms cubic-bezier(0.33, 0, 0.15, 1) both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .animate-cell-pop,\n  .animate-bar-rise,\n  .animate-chart-reveal {\n    animation: none;\n  }\n}\n\n/* ---------------------------------------------------------------------------\n * Table primitive (components/base/table)\n *\n * React Aria's collection components (Column/Row/Cell/…) can't be wrapped in\n * custom components without losing collection behaviour, so the primitive\n * re-exports them untouched and styling lives here, scoped to `.bui-table`.\n * React Aria renders real <table>/<thead>/<th>/<tr>/<td> elements.\n * --------------------------------------------------------------------------- */\n.bui-table {\n  @apply w-full border-collapse text-left;\n}\n/* Header background + border-y live on the cells: with border-collapse the\n * browser doesn't paint backgrounds/borders set on the <thead> row group.\n * Backgrounds/borders are set with explicit CSS (theme vars) rather than\n * @apply, which drops border-width/bg on these row-group cells. */\n.bui-table th {\n  @apply px-3 py-2.5 text-left align-middle text-body-medium text-text-tertiary whitespace-nowrap outline-none;\n  background-color: var(--color-background-secondary-default);\n  border-top: 1px solid var(--color-separator-border);\n  border-bottom: 1px solid var(--color-separator-border);\n  transition: padding 200ms ease, font-size 200ms ease;\n}\n.bui-table td {\n  @apply px-3 py-2.5 align-middle text-body-medium text-text-primary outline-none;\n  transition: padding 200ms ease, font-size 200ms ease;\n}\n.bui-table tbody tr {\n  @apply transition-colors duration-150;\n  border-bottom: 1px solid var(--color-separator-border);\n}\n.bui-table tbody tr[data-focus-visible] {\n  @apply ring-2 ring-inset ring-border-focus-ring;\n}\n.bui-table.bui-table-sm th,\n.bui-table.bui-table-sm td {\n  @apply px-2.5 py-1.5 text-body-2-medium;\n}\n\n/* Agent Thinking (components/application/agent-thinking): the agent thinking state\n * above a chat composer. The label reuses the ai-chat-text-shimmer travel;\n * its base/highlight both derive from the tone the component sets via\n * --bui-agent-thinking-tone, so every tone shimmers consistently. */\n.bui-agent-thinking-label {\n  color: transparent;\n  background-image: linear-gradient(\n    100deg,\n    color-mix(in oklab, var(--bui-agent-thinking-tone) 55%, transparent) 30%,\n    var(--bui-agent-thinking-tone) 50%,\n    color-mix(in oklab, var(--bui-agent-thinking-tone) 55%, transparent) 70%\n  );\n  background-position: 200% center;\n  background-size: 300% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  animation: ai-chat-text-shimmer 2.6s linear infinite;\n  will-change: background-position;\n}\n\n/* Star sparkle: scale + fade in, settle, and vanish; per-star duration and\n * stagger come in via inline animation-duration/-delay. */\n@keyframes bui-agent-thinking-star {\n  0%,\n  100% {\n    transform: scale(0);\n    opacity: 0;\n  }\n  40% {\n    transform: scale(1);\n    opacity: 1;\n  }\n  60% {\n    transform: scale(0.8);\n    opacity: 0.9;\n  }\n}\n\n.bui-agent-thinking-star {\n  transform: scale(0);\n  animation-name: bui-agent-thinking-star;\n  animation-timing-function: ease-in-out;\n  animation-iteration-count: infinite;\n}\n\n/* Infinity comet: the dash segment laps the normalized (pathLength=100)\n * figure-eight once per animation-duration. */\n@keyframes bui-agent-thinking-dash {\n  to {\n    stroke-dashoffset: -100;\n  }\n}\n\n.bui-agent-thinking-comet {\n  animation-name: bui-agent-thinking-dash;\n  animation-timing-function: linear;\n  animation-iteration-count: infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .bui-agent-thinking-label {\n    color: var(--bui-agent-thinking-tone);\n    background-image: none;\n    animation: none;\n  }\n\n  /* Stars rest visible instead of frozen at scale(0). */\n  .bui-agent-thinking-star {\n    animation: none;\n    transform: none;\n    opacity: 0.7;\n  }\n\n  .bui-agent-thinking-comet {\n    animation: none;\n  }\n}\n\n/* Star glyphs read a touch heavier than the dot grids on light backgrounds;\n * soften only them (not the label) there. Dark mode keeps the full tone. */\n.bui-agent-thinking-stars {\n  color: color-mix(in oklab, currentColor 75%, transparent);\n}\n\n.dark .bui-agent-thinking-stars {\n  color: inherit;\n}\n\n/* Composer Loader (components/application/composer-loader): the light band\n * orbiting the composer while the agent works. Dash segments on a\n * pathLength-normalized SVG stroke — the offset laps once per duration. */\n@keyframes bui-composer-loader-dash {\n  from {\n    stroke-dashoffset: 0;\n  }\n\n  to {\n    stroke-dashoffset: -100;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .bui-composer-loader-rect {\n    animation: none !important;\n  }\n}\n\n/* Composer mic listening state (components/application/ai-chat): equalizer\n * bars dancing while voice input is active. Per-bar duration/delay come in\n * inline so the bars drift out of phase like real audio. */\n@keyframes bui-composer-mic-bars {\n  0%,\n  100% {\n    transform: scaleY(0.4);\n  }\n\n  50% {\n    transform: scaleY(1);\n  }\n}\n\n.bui-composer-mic-bar {\n  animation: bui-composer-mic-bars 0.9s ease-in-out infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .bui-composer-mic-bar {\n    animation: none;\n    transform: scaleY(0.7);\n  }\n}\n\n/* Shimmer sweep on a primary call to action (the Pro prompt card). */\n@keyframes bui-badge-shimmer {\n  0% {\n    transform: translateX(0) skewX(-12deg);\n  }\n  40%,\n  100% {\n    transform: translateX(350%) skewX(-12deg);\n  }\n}\n\n.bui-badge-shimmer {\n  animation: bui-badge-shimmer 2.5s ease-in-out infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .bui-badge-shimmer {\n      animation: none;\n      opacity: 0;\n    }\n}\n\n/* Showcase card (docs index, landing, the starter's catalogue). */\n.landing-showcase-card {\n  transition: box-shadow 280ms ease;\n}\n\n.landing-showcase-card::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  z-index: 60;\n  pointer-events: none;\n  border: 1px solid var(--color-border-button-default);\n  border-radius: inherit;\n  transition:\n    border-color 280ms ease,\n    box-shadow 280ms ease;\n}\n\n.dark .landing-showcase-card::after {\n  border-color: rgb(255 255 255 / 0.08);\n}\n\n@media (min-width: 640px) and (hover: hover) {\n  .landing-showcase-card:hover::after,\n  .landing-showcase-card:focus-within::after {\n    border-color: var(--color-border-button-hover);\n    box-shadow: inset 0 0 0 2px var(--color-border-button-hover);\n  }\n}\n"
    },
    {
      "path": "components/boardui/styles/theme.css",
      "target": "@components/boardui/styles/theme.css",
      "type": "registry:file",
      "content": "/* ---------------------------------------------------------------------------\n * BoardUI theme tokens\n *\n * Layered as Tailwind v4 expects:\n *   @theme { ... }     primitives + ramp overrides → Tailwind utilities\n *   :root { ... }      semantic tokens (light mode) — reference primitives\n *   .dark { ... }      semantic overrides (dark mode) — landed when designed\n *   @theme inline {..} re-export of semantic tokens so they become utilities\n *\n * See docs/FIGMA_WORKFLOW.md for how tokens get added (Figma is source of truth).\n * --------------------------------------------------------------------------- */\n\n@theme {\n  /* ----- Color primitives ----------------------------------------------------\n   * Almost the entire palette matches Tailwind v4's OKLCH defaults. The four\n   * overrides below are the only intentional deviations (see Figma → variables\n   * → color/...).\n   * --------------------------------------------------------------------------*/\n  --color-slate-200:   #ebeff5;\n  --color-neutral-100: #f7f7f7;\n  --color-neutral-200: #ebebeb;\n  --color-neutral-925: #121212;\n  --color-blue-400:    #3392ff;\n\n  /* ----- Accent ramp ---------------------------------------------------------\n   * The CTA hue. Interactive/selection surfaces (primary button gradient,\n   * ghost + link buttons, checkbox, radio, switch, slider, tabs, sidebar\n   * selected, focus ring, date-range selection) reference accent-* instead of\n   * a raw hue, so the whole system re-tints by overriding these eleven\n   * variables at runtime (see components/application/theme/accent.ts) or in\n   * a customer project after importing this file. Defaults to the blue\n   * primitives above. Content/data blues (charts, status chips, calendar\n   * events, info notifications) deliberately keep referencing blue-*.\n   * --------------------------------------------------------------------------*/\n  --color-accent-50:  var(--color-blue-50);\n  --color-accent-100: var(--color-blue-100);\n  --color-accent-200: var(--color-blue-200);\n  --color-accent-300: var(--color-blue-300);\n  --color-accent-400: var(--color-blue-400);\n  --color-accent-500: var(--color-blue-500);\n  --color-accent-600: var(--color-blue-600);\n  --color-accent-700: var(--color-blue-700);\n  --color-accent-800: var(--color-blue-800);\n  --color-accent-900: var(--color-blue-900);\n  --color-accent-950: var(--color-blue-950);\n\n  /* ----- Typography ----------------------------------------------------------\n   * Font sizes, weights, line-heights, and tracking all match Tailwind v4\n   * defaults. The only override is the family: Inter, loaded via next/font\n   * in app/layout.tsx, which exposes --font-inter to the document.\n   * --------------------------------------------------------------------------*/\n  --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif,\n               \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n               \"Noto Color Emoji\";\n\n  /* Monospace ramp for code blocks. Loaded via next/font in app/layout.tsx\n   * (--font-mono-source → JetBrains Mono). Figma's spec uses IBM Plex Mono;\n   * JetBrains Mono is the closest already-bundled face. */\n  --font-mono: var(--font-mono-source), ui-monospace, \"SFMono-Regular\", Menlo,\n               Consolas, \"Liberation Mono\", monospace;\n\n  /* ----- Radii ---------------------------------------------------------------\n   * Extra step between lg (8) and xl (12). Used by medium-size buttons.\n   * Figma name: radius/2lg.\n   * --------------------------------------------------------------------------*/\n  --radius-2lg: 10px;\n  --radius-notification-card: 10px;\n\n  /* ----- Shadows -------------------------------------------------------------\n   * 2xs, xs, 2xl and the inner shadows already match Tailwind v4. Sizes sm, md,\n   * lg, xl follow the Figma 6%/10% recipe (slightly softer than Tailwind v4's\n   * 10%/10% default).\n   * --------------------------------------------------------------------------*/\n  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.06), 0 1px 3px 0 rgb(0 0 0 / 0.10);\n  --shadow-md: 0 2px 4px -1px rgb(0 0 0 / 0.06), 0 4px 6px -1px rgb(0 0 0 / 0.10);\n  --shadow-lg: 0 4px 6px -2px rgb(0 0 0 / 0.05), 0 10px 15px -3px rgb(0 0 0 / 0.10);\n  --shadow-xl: 0 10px 10px -5px rgb(0 0 0 / 0.04), 0 20px 25px -5px rgb(0 0 0 / 0.10);\n\n  /* Figma effect style \"Background/Sidebar Elevation\" — copied 1:1:\n   *   DROP_SHADOW #00000005 offset(0,1) radius 0  spread 0  → 0 1px 0px  0 / 0.0196\n   *   DROP_SHADOW #0000000F offset(0,1) radius 12 spread 0  → 0 1px 12px 0 / 0.0588\n   *   DROP_SHADOW #00000052 offset(0,0) radius 1  spread 0  → 0 0   1px  0 / 0.3216\n   * (alpha = hex byte / 255: 05→0.0196, 0F→0.0588, 52→0.3216). */\n  --shadow-sidebar:\n    0 1px 0 0 rgb(0 0 0 / 0.0196),\n    0 1px 12px 0 rgb(0 0 0 / 0.0588),\n    0 0 1px 0 rgb(0 0 0 / 0.3216);\n\n  /* Figma effect style \"Sidebar selected\" — copied 1:1:\n   *   DROP_SHADOW  color/blue/500 offset(0,0) radius 0 spread 1 → 0 0 0 1px blue-500\n   *   INNER_SHADOW #FFFFFF40       offset(0,1) radius 0 spread 0 → inset 0 1px 0 0 / 0.251\n   * (#FFFFFF40 alpha = 0x40/255 = 0.251). */\n  --shadow-nav-selected:\n    0 0 0 1px var(--color-accent-500),\n    inset 0 1px 0 0 rgb(255 255 255 / 0.251);\n\n  /* Checkbox checked/indeterminate surface (from the Figma Checkbox component):\n   * inner top highlight + 1px inner blue edge over the primary gradient. */\n  --shadow-checkbox-selected:\n    inset 0 2px 0 0 rgb(255 255 255 / 0.25),\n    inset 0 0 0 1px var(--color-accent-500);\n\n  /* 1px contact shadow used on white cards/dropdown triggers sitting on the\n   * secondary background (Figma drop shadow 0 1 1 5%). */\n  --shadow-card: 0 1px 1px 0 rgb(0 0 0 / 0.05);\n\n  /* Floating menu/dropdown panel (Figma → sidebar profile menu node 3823:3699):\n   *   DROP_SHADOW #0000000A offset(0,1) radius 1 → 0 1px 1px 0 / 0.04\n   *   DROP_SHADOW #00000005 offset(0,4) radius 4 → 0 4px 4px 0 / 0.02 */\n  --shadow-dropdown:\n    0 1px 1px 0 rgb(0 0 0 / 0.04),\n    0 4px 4px 0 rgb(0 0 0 / 0.02);\n\n  /* Floating template waitlist (Figma node 4117:32737). This extends the\n   * sidebar elevation recipe with the two broad ambient layers used by the\n   * signup surface. */\n  --shadow-waitlist:\n    0 0 6px 0 rgb(0 0 0 / 0.06),\n    0 0 64px 0 rgb(0 0 0 / 0.16),\n    0 0 1px 0 rgb(0 0 0 / 0.32),\n    0 1px 12px 0 rgb(0 0 0 / 0.06),\n    0 1px 0 0 rgb(0 0 0 / 0.02);\n\n  /* Inner shadows (Figma names inner-shadow/*; Tailwind utility prefix is\n   * inset-shadow-*).\n   */\n  --inset-shadow-2xs: inset 0 1px 0 0 rgb(0 0 0 / 0.05);\n  --inset-shadow-xs:  inset 0 1px 1px 0 rgb(0 0 0 / 0.05);\n  --inset-shadow-sm:  inset 0 2px 4px 0 rgb(0 0 0 / 0.05);\n}\n\n/* ---------------------------------------------------------------------------\n * Semantic tokens — light mode.\n *\n * All values reference primitives via var(--color-{family}-{shade}). Edit the\n * primitives in @theme above and these update automatically.\n *\n * Figma name on the left, var name on the right:\n *   foreground/full                  → --color-foreground-full\n *   foreground/icon/primary          → --color-foreground-icon-primary\n *   foreground/disabled              → --color-foreground-disabled\n *   foreground/disabled-danger       → --color-foreground-disabled-danger\n *   text/primary                     → --color-text-primary\n *   text/tertiary                    → --color-text-tertiary\n *   text/disabled-danger             → --color-text-disabled-danger\n *   background/primary/default       → --color-background-primary-default\n *   background/primary/hover         → --color-background-primary-hover\n *   background/primary/active        → --color-background-primary-active\n *   background/primary/disabled      → --color-background-primary-disabled\n *   background/secondary/default     → --color-background-secondary-default\n *   border/button/default            → --color-border-button-default\n *   border/button/hover              → --color-border-button-hover\n *   border/button/active             → --color-border-button-active\n *\n * Button gradients (linear, 180°, light stop → dark stop) sourced from the\n * blue/red primitives that each Button/{type}/{state} style references in\n * Figma. Direction inferred — confirm visually against Figma.\n * --------------------------------------------------------------------------- */\n:root {\n  /* Foreground (icons + on-fill content) */\n  --color-foreground-full:               var(--color-white);\n  --color-foreground-icon-primary:       var(--color-neutral-950);\n  --color-foreground-icon-hover:         var(--color-black);\n  --color-foreground-icon-secondary:     var(--color-neutral-500);\n  --color-foreground-icon-tertiary:      var(--color-neutral-400);\n  --color-foreground-icon-quaternary:    var(--color-neutral-300);\n  --color-foreground-icon-error:         var(--color-red-600);\n  --color-foreground-icon-disabled:      var(--color-neutral-300);\n  --color-foreground-disabled:           var(--color-neutral-400);\n  --color-foreground-disabled-danger:    var(--color-red-300);\n  --color-input-disabled-foreground:     var(--color-neutral-300);\n  --color-icon-button-disabled-foreground: var(--color-neutral-300);\n  --color-button-primary-disabled-foreground: var(--color-neutral-400);\n\n  /* Text */\n  --color-text-primary:         var(--color-neutral-950);\n  --color-text-secondary:       var(--color-neutral-600);\n  --color-text-tertiary:        #737373;\n  --color-text-disabled:        var(--color-neutral-300);\n  --color-text-disabled-danger: var(--color-red-300);\n  --color-text-placeholder:     var(--color-neutral-400);\n  --color-text-error-primary:   var(--color-red-500);\n  --color-input-disabled-text:  var(--color-neutral-300);\n  --color-text-white:           var(--color-white);\n\n  /* Background */\n  --color-background-full:              var(--color-white);\n  --color-background-primary-default:   var(--color-white);\n  --color-background-inner-default:     var(--color-white);\n  --color-background-primary-hover:     var(--color-neutral-100);\n  --color-background-primary-active:    var(--color-neutral-200);\n  --color-background-primary-disabled:  var(--color-neutral-100);\n  --color-input-disabled-background:    var(--color-neutral-100);\n  --color-background-secondary-default: var(--color-neutral-100);\n  --color-background-secondary-hover:   var(--color-neutral-200);\n  --color-background-tertiary-default:  var(--color-neutral-200);\n  --color-background-tertiary-hover:    var(--color-neutral-300);\n  --color-background-tertiary-error:    var(--color-red-100);\n  --color-background-recent-hire-role:  var(--color-neutral-100);\n  --color-avatar-neutral-background:    var(--color-neutral-300);\n  /* Figma background/quaternary/default — first used by the Storage\n   * dropzone's upload-icon disc (node 4106:26587). */\n  --color-background-quaternary-default: var(--color-neutral-300);\n  --color-background-quaternary-hover:   var(--color-neutral-400);\n  --color-background-quaternary-error:   var(--color-red-200);\n  --color-file-upload-icon-background:   var(--color-neutral-300);\n  --color-file-upload-icon-foreground:   var(--color-neutral-400);\n  --color-file-upload-icon-foreground-hover: var(--color-neutral-500);\n\n  /* Border */\n  --color-border-button-default:   var(--color-neutral-200);\n  --color-border-button-hover:     var(--color-neutral-300);\n  --color-border-button-active:    var(--color-neutral-400);\n  --color-border-button-white:     var(--color-white);\n  /* The system had no red border. Matches text/error/primary so an invalid\n     field's edge and its message are the same red. */\n  --color-border-error-default:    var(--color-red-500);\n  --color-border-checkbox-default: var(--color-neutral-300);\n  --color-border-checkbox-hover:   var(--color-neutral-400);\n  --color-border-checkbox-active:  var(--color-neutral-400);\n  --color-border-checkbox-white:   var(--color-white);\n  --color-border-component-detail-container: var(--color-neutral-200);\n  --color-border-ai-profile-card:             var(--color-neutral-200);\n  --color-border-table:            var(--color-neutral-200);\n  --color-border-button-group:     var(--color-neutral-200);\n  --color-border-sidebar-profile-hover: var(--color-neutral-300);\n  --color-separator-border:        var(--color-neutral-200);\n  --color-separator-border-strong: var(--color-neutral-200);\n  --color-border-focus-ring:       var(--color-accent-500);\n\n  /* State */\n  --color-state-success-text: var(--color-lime-800);\n  --color-state-success-base: var(--color-lime-200);\n\n  /* Selected control interiors remain light on colored tracks in both themes. */\n  --color-control-indicator-background:        var(--color-white);\n  --color-control-indicator-background-subtle: var(--color-neutral-100);\n  --color-switch-off-chip-start:                var(--color-white);\n  --color-switch-off-chip-end:                  var(--color-neutral-100);\n  /* Selected switch chip: the Figma comps use hexes a touch deeper than the\n   * plain 500→600 ramp (#2473fe → #0450e2 for blue). Expressed as oklab mixes\n   * of neighbouring accent stops so the recipe survives a re-tint. */\n  --color-switch-on-chip-start:                 color-mix(in oklab, var(--color-accent-500) 63%, var(--color-accent-600));\n  --color-switch-on-chip-end:                   color-mix(in oklab, var(--color-accent-600) 16%, var(--color-accent-700));\n  --color-stat-card-icon-background:            var(--color-white);\n  --color-segmented-control-background:          var(--color-neutral-100);\n  --color-segmented-control-selected-background: var(--color-white);\n  --color-theme-toggle-sidebar-background:       var(--color-neutral-200);\n  --color-theme-toggle-sidebar-selected-background: var(--color-white);\n  /* Stepped against the composer: the pill is white and the add button is the\n   * subtle grey sitting on it, so the control separates from the surface it\n   * shares an edge with. Dark mode keeps its own pair, where the pill is the\n   * lighter of the two and the button steps the other way. */\n  --color-ai-chat-composer-add-background:       var(--color-neutral-100);\n  --color-ai-chat-composer-add-hover-background: var(--color-neutral-200);\n  --color-agent-progress-ring:                     var(--color-neutral-700);\n  --color-button-ghost-background: transparent;\n  --color-button-ghost-hover: var(--color-background-primary-hover);\n  --color-button-ghost-active: var(--color-background-primary-active);\n  --color-button-ghost-disabled: transparent;\n  --color-button-ghost-foreground: var(--color-text-primary);\n  --color-button-ghost-disabled-foreground: var(--color-text-tertiary);\n  --color-dropdown-item-hover-background:        var(--color-neutral-100);\n  --color-kbd-background:                        var(--color-neutral-300);\n  --color-kbd-foreground:                        var(--color-neutral-500);\n  --color-docs-file-chip-background:             var(--color-purple-50);\n  --color-docs-file-chip-border:                 var(--color-purple-100);\n  --color-docs-file-chip-foreground:             var(--color-purple-500);\n  --color-docs-command-accent:                    var(--color-purple-400);\n  --color-docs-code-background:                   var(--color-background-primary-default);\n  --color-docs-install-rail:                      var(--color-border-button-default);\n\n  /* Recently shipped component badge. Kept semantic so its compact blue\n   * surface can retain the same hierarchy in both themes. */\n  --color-badge-neutral-background: var(--color-neutral-200);\n  --color-team-menu-count-background: var(--color-neutral-200);\n  --color-team-menu-count-foreground: var(--color-neutral-500);\n  --color-badge-new-background: var(--color-accent-100);\n  --color-badge-new-text:       var(--color-accent-600);\n  --color-tab-count-selected-background: var(--color-accent-100);\n  --color-pill-tab-blue-selected-background: var(--color-accent-50);\n  --color-pill-tab-blue-hover-background: var(--color-neutral-100);\n\n  /* Notification status-icon surfaces. */\n  --color-notification-information-background: var(--color-blue-100);\n  --color-notification-information-foreground: var(--color-blue-600);\n  --color-notification-success-background:     var(--color-lime-100);\n  --color-notification-success-foreground:     var(--color-lime-600);\n  --color-notification-error-background:       var(--color-rose-100);\n  --color-notification-error-foreground:       var(--color-rose-600);\n  --color-notification-center-background:      var(--color-white);\n\n  /* Table status chips. Dark mappings come from the medical dashboard table\n   * in Figma (4139:7202, 4139:7240, 4139:7278, 4139:7315). */\n  --color-status-lime-background:   var(--color-lime-200);\n  --color-status-lime-text:         var(--color-lime-800);\n  --color-status-yellow-background: var(--color-yellow-200);\n  --color-status-yellow-text:       var(--color-yellow-800);\n  --color-status-rose-background:   var(--color-rose-200);\n  --color-status-rose-text:         var(--color-rose-800);\n  --color-status-cyan-background:   var(--color-cyan-200);\n  --color-status-cyan-text:         var(--color-cyan-800);\n  --color-status-blue-background:   var(--color-blue-200);\n  --color-status-blue-text:         var(--color-blue-800);\n  --color-status-purple-background: var(--color-purple-100);\n  --color-status-purple-text:       var(--color-purple-600);\n  --color-status-dot-green-halo:    var(--color-green-100);\n  --color-status-dot-yellow-halo:   var(--color-yellow-200);\n  --color-status-dot-indigo-halo:   var(--color-indigo-100);\n\n  /* Date range selection: the connecting band and its start/end caps. */\n  --color-date-range-background:      var(--color-accent-100);\n  --color-date-range-edge-background: var(--color-accent-300);\n\n  /* Calendar event chips. Title/time stay separate so the original light\n   * hierarchy remains intact while dark mode can normalize both to /300. */\n  --color-calendar-event-blue-background:    var(--color-blue-100);\n  --color-calendar-event-blue-title:         var(--color-blue-700);\n  --color-calendar-event-blue-time:          var(--color-blue-700);\n  --color-calendar-event-pink-background:    var(--color-pink-100);\n  --color-calendar-event-pink-title:         var(--color-pink-700);\n  --color-calendar-event-pink-time:          var(--color-pink-700);\n  --color-calendar-event-purple-background:  var(--color-purple-100);\n  --color-calendar-event-purple-title:       var(--color-purple-700);\n  --color-calendar-event-purple-time:        var(--color-purple-700);\n  --color-calendar-event-lime-background:    var(--color-lime-100);\n  --color-calendar-event-lime-title:         var(--color-lime-800);\n  --color-calendar-event-lime-time:          var(--color-lime-700);\n  --color-calendar-event-emerald-background: var(--color-emerald-100);\n  --color-calendar-event-emerald-title:      var(--color-emerald-800);\n  --color-calendar-event-emerald-time:       var(--color-emerald-700);\n\n  /* Placeholder colors that don't map cleanly to existing text tokens */\n  --color-text-error-placeholder: var(--color-red-400);\n\n  /* Charts (graphs) — shared neutrals + a categorical accent ramp.\n   * `track` is the background bar/ring behind a series; `cursor` the hover\n   * outline / dashed reference line; `neutral` a data color for \"other\"-style\n   * slices. Each numbered accent pairs a base fill with a one-tone-darker\n   * `-active` used for hover emphasis (the 400 → 500 recipe every chart\n   * follows). Numbered (not hue-named) so the palette can be retuned without\n   * renaming, mirroring the chart-1…n convention common to chart systems. */\n  --color-chart-track:    var(--color-neutral-200);\n  --color-chart-cursor:   var(--color-neutral-300);\n  --color-chart-neutral:  var(--color-neutral-300);\n  --color-chart-1:        var(--color-teal-400);\n  --color-chart-1-active: var(--color-teal-500);\n  --color-chart-2:        var(--color-lime-400);\n  --color-chart-2-active: var(--color-lime-500);\n  --color-chart-3:        var(--color-pink-400);\n  --color-chart-3-active: var(--color-pink-500);\n  --color-chart-4:        var(--color-sky-400);\n  --color-chart-4-active: var(--color-sky-500);\n  --color-chart-5:        var(--color-purple-400);\n  --color-chart-5-active: var(--color-purple-500);\n  --color-chart-6:        var(--color-blue-400);\n  --color-chart-6-active: var(--color-blue-500);\n  --color-chart-7:        var(--color-emerald-400);\n  --color-chart-7-active: var(--color-emerald-500);\n  --color-chart-8:        var(--color-yellow-400);\n  --color-chart-8-active: var(--color-yellow-500);\n  --color-chart-9:        var(--color-indigo-400);\n  --color-chart-9-active: var(--color-indigo-500);\n  --color-chart-agents-bar:        var(--color-purple-300);\n  --color-chart-agents-bar-active: var(--color-purple-400);\n\n  /* Button gradients */\n  --gradient-button-primary-default:  linear-gradient(180deg, var(--color-accent-500)    0%, var(--color-accent-600)    100%);\n  --gradient-button-primary-hover:    linear-gradient(180deg, var(--color-accent-400)    0%, var(--color-accent-500)    100%);\n  --gradient-button-primary-active:   linear-gradient(180deg, var(--color-accent-600)    0%, var(--color-accent-700)    100%);\n  --gradient-button-primary-disabled: linear-gradient(180deg, var(--color-neutral-200) 0%, var(--color-neutral-300) 100%);\n\n  --gradient-button-danger-default:   linear-gradient(180deg, var(--color-red-500) 0%, var(--color-red-600) 100%);\n  --gradient-button-danger-hover:     linear-gradient(180deg, var(--color-red-400) 0%, var(--color-red-500) 100%);\n  --gradient-button-danger-active:    linear-gradient(180deg, var(--color-red-600) 0%, var(--color-red-700) 100%);\n  --gradient-button-danger-disabled:  linear-gradient(180deg, var(--color-red-100) 0%, var(--color-red-200) 100%);\n}\n\n.dark {\n  /* Foreground */\n  --color-foreground-full:            var(--color-neutral-700);\n  --color-foreground-icon-primary:    var(--color-neutral-50);\n  --color-foreground-icon-hover:      var(--color-white);\n  --color-foreground-icon-error:      var(--color-red-400);\n  --color-foreground-icon-tertiary:   var(--color-neutral-600);\n  --color-foreground-icon-quaternary: var(--color-neutral-700);\n  --color-foreground-icon-disabled:   var(--color-neutral-800);\n  --color-foreground-disabled:        var(--color-neutral-700);\n  --color-foreground-disabled-danger: var(--color-red-400);\n  --color-input-disabled-foreground:  var(--color-neutral-600);\n  --color-icon-button-disabled-foreground: var(--color-neutral-500);\n  --color-button-primary-disabled-foreground: var(--color-neutral-500);\n\n  /* Text */\n  --color-text-primary:  var(--color-neutral-50);\n  --color-text-tertiary: var(--color-neutral-600);\n  --color-text-disabled: var(--color-neutral-800);\n  --color-text-error-primary: var(--color-red-400);\n  --color-input-disabled-text: color-mix(in srgb, var(--color-neutral-500) 40%, transparent);\n\n  /* Background */\n  --color-background-full:              var(--color-neutral-925);\n  --color-background-primary-default:   #1c1c1c;\n  --color-background-inner-default:     color-mix(in srgb, var(--color-neutral-800) 60%, transparent);\n  --color-background-primary-hover:     color-mix(in srgb, var(--color-neutral-700) 60%, transparent);\n  --color-background-primary-active:    var(--color-neutral-800);\n  --color-background-primary-disabled:  var(--color-neutral-800);\n  --color-input-disabled-background:    color-mix(in srgb, var(--color-neutral-800) 30%, transparent);\n  --color-switch-off-chip-start:        var(--color-background-tertiary-default);\n  --color-switch-off-chip-end:          var(--color-background-tertiary-default);\n  --color-background-secondary-default: #171717;\n  --color-background-secondary-hover:   var(--color-neutral-800);\n  --color-background-tertiary-default:  var(--color-neutral-800);\n  --color-background-tertiary-hover:    var(--color-neutral-700);\n  --color-background-tertiary-error:    color-mix(in srgb, var(--color-red-950) 60%, transparent);\n  --color-background-quaternary-default: var(--color-neutral-700);\n  --color-background-quaternary-hover:   var(--color-neutral-600);\n  --color-background-recent-hire-role:  color-mix(in srgb, var(--color-neutral-900) 60%, transparent);\n  --color-avatar-neutral-background:    var(--color-background-primary-default);\n  --color-file-upload-icon-background:  var(--color-neutral-600);\n  --color-file-upload-icon-foreground:  var(--color-neutral-400);\n  --color-file-upload-icon-foreground-hover: var(--color-neutral-300);\n  --color-stat-card-icon-background:    var(--color-neutral-800);\n  --color-segmented-control-background:          var(--color-neutral-925);\n  --color-segmented-control-selected-background: var(--color-neutral-800);\n  --color-theme-toggle-sidebar-background:       var(--color-neutral-800);\n  --color-theme-toggle-sidebar-selected-background: var(--color-neutral-700);\n  --color-ai-chat-composer-add-background:       var(--color-neutral-700);\n  --color-ai-chat-composer-add-hover-background: var(--color-neutral-600);\n  --color-agent-progress-ring:                     color-mix(in srgb, var(--color-white) 50%, transparent);\n  --color-button-ghost-background: transparent;\n  --color-button-ghost-hover: var(--color-background-primary-hover);\n  --color-button-ghost-active: var(--color-background-primary-active);\n  --color-button-ghost-disabled: transparent;\n  --color-button-ghost-foreground: var(--color-text-primary);\n  --color-button-ghost-disabled-foreground: var(--color-text-tertiary);\n  --color-dropdown-item-hover-background:        color-mix(in srgb, var(--color-neutral-700) 60%, transparent);\n  --color-kbd-background:                        var(--color-neutral-700);\n  --color-kbd-foreground:                        var(--color-neutral-400);\n  --color-docs-file-chip-background:             color-mix(in srgb, var(--color-purple-950) 50%, transparent);\n  --color-docs-file-chip-border:                 var(--color-purple-800);\n  --color-docs-file-chip-foreground:             var(--color-purple-400);\n  --color-docs-code-background:                  var(--color-background-full);\n  --color-docs-install-rail:                     color-mix(in srgb, var(--color-neutral-800) 60%, transparent);\n\n  /* Dark surfaces absorb low-alpha shadows more readily than white ones.\n   * Preserve the light-mode geometry and raise only the black alpha: tighter\n   * contact shadows stay crisp, while higher elevations receive a restrained\n   * ambient layer without introducing a grey glow. */\n  --shadow-2xs: 0 1px rgb(0 0 0 / 0.16);\n  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.18);\n  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.12), 0 1px 3px 0 rgb(0 0 0 / 0.16);\n  --shadow-md: 0 2px 4px -1px rgb(0 0 0 / 0.12), 0 4px 6px -1px rgb(0 0 0 / 0.18);\n  --shadow-lg: 0 4px 6px -2px rgb(0 0 0 / 0.10), 0 10px 15px -3px rgb(0 0 0 / 0.18);\n  --shadow-xl: 0 10px 10px -5px rgb(0 0 0 / 0.08), 0 20px 25px -5px rgb(0 0 0 / 0.20);\n  --shadow-card: 0 1px 1px 0 rgb(0 0 0 / 0.14);\n  --shadow-dropdown:\n    0 1px 1px 0 rgb(0 0 0 / 0.14),\n    0 4px 4px 0 rgb(0 0 0 / 0.10);\n  --shadow-sidebar:\n    0 1px 0 0 rgb(0 0 0 / 0.10),\n    0 1px 12px 0 rgb(0 0 0 / 0.16),\n    0 0 1px 0 rgb(0 0 0 / 0.42);\n  --shadow-waitlist:\n    0 0 6px 0 rgb(0 0 0 / 0.12),\n    0 0 64px 0 rgb(0 0 0 / 0.22),\n    0 0 1px 0 rgb(0 0 0 / 0.42),\n    0 1px 12px 0 rgb(0 0 0 / 0.14),\n    0 1px 0 0 rgb(0 0 0 / 0.08);\n\n  /* Badges */\n  --color-badge-neutral-background: var(--color-neutral-800);\n  --color-team-menu-count-background: var(--color-neutral-700);\n  --color-team-menu-count-foreground: var(--color-neutral-400);\n  --color-badge-new-background: var(--color-accent-950);\n  --color-badge-new-text:       var(--color-accent-400);\n  --color-tab-count-selected-background: color-mix(in srgb, var(--color-accent-800) 60%, transparent);\n\n  /* Dark status discs use /800 surfaces with /300 foregrounds so the icon\n   * remains crisp without reverting to a bright pastel circle. */\n  --color-notification-information-background: color-mix(in srgb, var(--color-blue-800) 50%, transparent);\n  --color-notification-information-foreground: var(--color-blue-300);\n  --color-notification-success-background:     color-mix(in srgb, var(--color-lime-800) 50%, transparent);\n  --color-notification-success-foreground:     var(--color-lime-300);\n  --color-notification-error-background:       color-mix(in srgb, var(--color-rose-800) 50%, transparent);\n  --color-notification-error-foreground:       var(--color-rose-300);\n  --color-notification-center-background:      var(--color-neutral-900);\n\n  /* Borders and separators */\n  --color-border-button-default:   var(--color-neutral-700);\n  --color-border-button-hover:     var(--color-neutral-500);\n  --color-border-button-active:    var(--color-neutral-600);\n  --color-border-button-white:     var(--color-zinc-800);\n  --color-border-error-default:    var(--color-red-400);\n  --color-border-checkbox-default: var(--color-neutral-700);\n  --color-border-checkbox-hover:   var(--color-neutral-500);\n  --color-border-checkbox-active:  var(--color-neutral-500);\n  --color-border-component-detail-container: var(--color-neutral-800);\n  --color-border-ai-profile-card:             var(--color-neutral-800);\n  --color-border-table:            var(--color-neutral-800);\n  --color-border-button-group:     color-mix(in srgb, var(--color-neutral-600) 60%, transparent);\n  --color-border-sidebar-profile-hover: var(--color-neutral-600);\n  --color-separator-border:        var(--color-neutral-800);\n  --color-separator-border-strong: color-mix(in srgb, var(--color-neutral-700) 60%, transparent);\n\n  /* Figma's dark table status pairs. */\n  --color-status-lime-background:   color-mix(in srgb, var(--color-lime-950) 60%, transparent);\n  --color-status-lime-text:         var(--color-lime-500);\n  --color-status-yellow-background: color-mix(in srgb, var(--color-yellow-950) 60%, transparent);\n  --color-status-yellow-text:       var(--color-yellow-500);\n  --color-status-rose-background:   color-mix(in srgb, var(--color-rose-950) 60%, transparent);\n  --color-status-rose-text:         var(--color-rose-500);\n  --color-status-cyan-background:   color-mix(in srgb, var(--color-cyan-950) 60%, transparent);\n  --color-status-cyan-text:         var(--color-cyan-400);\n  --color-status-blue-background:   color-mix(in srgb, var(--color-blue-950) 60%, transparent);\n  --color-status-blue-text:         var(--color-blue-300);\n  --color-status-purple-background: color-mix(in srgb, var(--color-purple-900) 50%, transparent);\n  --color-status-purple-text:       var(--color-purple-300);\n  --color-status-dot-green-halo:    color-mix(in srgb, var(--color-green-800) 40%, transparent);\n  --color-status-dot-yellow-halo:   color-mix(in srgb, var(--color-yellow-800) 40%, transparent);\n  --color-status-dot-indigo-halo:   color-mix(in srgb, var(--color-indigo-800) 40%, transparent);\n\n  --color-date-range-background:      var(--color-accent-950);\n  --color-date-range-edge-background: var(--color-accent-800);\n\n  --color-pill-tab-blue-selected-background: color-mix(in srgb, var(--color-accent-950) 60%, transparent);\n  --color-pill-tab-blue-hover-background: var(--color-neutral-800);\n\n  /* Calendar event families share the same dark-mode depth/contrast recipe. */\n  --color-calendar-event-blue-background:    var(--color-blue-950);\n  --color-calendar-event-blue-title:         var(--color-blue-300);\n  --color-calendar-event-blue-time:          var(--color-blue-300);\n  --color-calendar-event-pink-background:    var(--color-pink-950);\n  --color-calendar-event-pink-title:         var(--color-pink-300);\n  --color-calendar-event-pink-time:          var(--color-pink-300);\n  --color-calendar-event-purple-background:  var(--color-purple-950);\n  --color-calendar-event-purple-title:       var(--color-purple-300);\n  --color-calendar-event-purple-time:        var(--color-purple-300);\n  --color-calendar-event-lime-background:    var(--color-lime-950);\n  --color-calendar-event-lime-title:         var(--color-lime-300);\n  --color-calendar-event-lime-time:          var(--color-lime-300);\n  --color-calendar-event-emerald-background: var(--color-emerald-950);\n  --color-calendar-event-emerald-title:      var(--color-emerald-300);\n  --color-calendar-event-emerald-time:       var(--color-emerald-300);\n\n  /* Disabled primary CTA: dark, low-emphasis fill with readable muted text. */\n  --gradient-button-primary-disabled: linear-gradient(\n    180deg,\n    var(--color-neutral-700) 0%,\n    var(--color-neutral-800) 100%\n  );\n  --gradient-button-danger-disabled: linear-gradient(\n    180deg,\n    var(--color-red-900) 0%,\n    var(--color-red-950) 100%\n  );\n\n  /* Neutral graph structure; categorical and status colors stay unchanged. */\n  --color-chart-track:   var(--color-neutral-800);\n  --color-chart-cursor:  var(--color-neutral-700);\n  --color-chart-neutral: var(--color-neutral-800);\n  --color-chart-agents-bar:        var(--color-purple-500);\n  --color-chart-agents-bar-active: var(--color-purple-600);\n}\n\n/* Non-default accents (data-accent set by the accent engine) can land on a\n * /950 that glows too loudly against the dark sidebar; halve the NEW badge\n * fill there. Light mode keeps the solid /100 surface. */\n.dark[data-accent] {\n  --color-badge-new-background: color-mix(in srgb, var(--color-accent-950) 50%, transparent);\n}\n\n/* Re-export semantic tokens as Tailwind utilities. The `inline` form means\n * Tailwind reads the *current* computed value of the variable, so light↔dark\n * flips work without rebuild. */\n@theme inline {\n  --color-foreground-full:            var(--color-foreground-full);\n  --color-foreground-icon-primary:    var(--color-foreground-icon-primary);\n  --color-foreground-icon-hover:      var(--color-foreground-icon-hover);\n  --color-foreground-icon-secondary:  var(--color-foreground-icon-secondary);\n  --color-foreground-icon-tertiary:   var(--color-foreground-icon-tertiary);\n  --color-foreground-icon-quaternary: var(--color-foreground-icon-quaternary);\n  --color-foreground-icon-error:      var(--color-foreground-icon-error);\n  --color-foreground-icon-disabled:   var(--color-foreground-icon-disabled);\n  --color-foreground-disabled:        var(--color-foreground-disabled);\n  --color-foreground-disabled-danger: var(--color-foreground-disabled-danger);\n  --color-input-disabled-foreground:  var(--color-input-disabled-foreground);\n  --color-icon-button-disabled-foreground: var(--color-icon-button-disabled-foreground);\n  --color-button-primary-disabled-foreground: var(--color-button-primary-disabled-foreground);\n\n  --color-text-primary:         var(--color-text-primary);\n  --color-text-secondary:       var(--color-text-secondary);\n  --color-text-tertiary:        var(--color-text-tertiary);\n  --color-text-disabled:        var(--color-text-disabled);\n  --color-text-disabled-danger: var(--color-text-disabled-danger);\n  --color-text-placeholder:     var(--color-text-placeholder);\n  --color-text-error-primary:   var(--color-text-error-primary);\n  --color-input-disabled-text:  var(--color-input-disabled-text);\n  --color-text-white:           var(--color-text-white);\n\n  --color-background-full:              var(--color-background-full);\n  --color-background-primary-default:   var(--color-background-primary-default);\n  --color-background-inner-default:     var(--color-background-inner-default);\n  --color-background-primary-hover:     var(--color-background-primary-hover);\n  --color-background-primary-active:    var(--color-background-primary-active);\n  --color-background-primary-disabled:  var(--color-background-primary-disabled);\n  --color-input-disabled-background:    var(--color-input-disabled-background);\n  --color-background-secondary-default: var(--color-background-secondary-default);\n  --color-background-secondary-hover:   var(--color-background-secondary-hover);\n  --color-background-tertiary-default:  var(--color-background-tertiary-default);\n  --color-background-tertiary-hover:    var(--color-background-tertiary-hover);\n  --color-background-tertiary-error:    var(--color-background-tertiary-error);\n  --color-background-recent-hire-role:  var(--color-background-recent-hire-role);\n  --color-avatar-neutral-background:    var(--color-avatar-neutral-background);\n  --color-background-quaternary-default: var(--color-background-quaternary-default);\n  --color-background-quaternary-hover:   var(--color-background-quaternary-hover);\n  --color-background-quaternary-error:   var(--color-background-quaternary-error);\n  --color-file-upload-icon-background:   var(--color-file-upload-icon-background);\n  --color-file-upload-icon-foreground:   var(--color-file-upload-icon-foreground);\n  --color-file-upload-icon-foreground-hover: var(--color-file-upload-icon-foreground-hover);\n\n  --color-border-button-default:   var(--color-border-button-default);\n  --color-border-button-hover:     var(--color-border-button-hover);\n  --color-border-button-active:    var(--color-border-button-active);\n  --color-border-button-white:     var(--color-border-button-white);\n  --color-border-error-default:    var(--color-border-error-default);\n  --color-border-checkbox-default: var(--color-border-checkbox-default);\n  --color-border-checkbox-hover:   var(--color-border-checkbox-hover);\n  --color-border-checkbox-active:  var(--color-border-checkbox-active);\n  --color-border-checkbox-white:   var(--color-border-checkbox-white);\n  --color-border-component-detail-container: var(--color-border-component-detail-container);\n  --color-border-ai-profile-card:             var(--color-border-ai-profile-card);\n  --color-border-table:            var(--color-border-table);\n  --color-border-button-group:     var(--color-border-button-group);\n  --color-border-sidebar-profile-hover: var(--color-border-sidebar-profile-hover);\n  --color-separator-border:        var(--color-separator-border);\n  --color-separator-border-strong: var(--color-separator-border-strong);\n  --color-border-focus-ring:       var(--color-border-focus-ring);\n\n  --color-state-success-text: var(--color-state-success-text);\n  --color-state-success-base: var(--color-state-success-base);\n  --color-control-indicator-background:        var(--color-control-indicator-background);\n  --color-control-indicator-background-subtle: var(--color-control-indicator-background-subtle);\n  --color-switch-off-chip-start:                var(--color-switch-off-chip-start);\n  --color-switch-off-chip-end:                  var(--color-switch-off-chip-end);\n  --color-switch-on-chip-start:                 var(--color-switch-on-chip-start);\n  --color-switch-on-chip-end:                   var(--color-switch-on-chip-end);\n  --color-stat-card-icon-background:            var(--color-stat-card-icon-background);\n  --color-segmented-control-background:          var(--color-segmented-control-background);\n  --color-segmented-control-selected-background: var(--color-segmented-control-selected-background);\n  --color-theme-toggle-sidebar-background:       var(--color-theme-toggle-sidebar-background);\n  --color-theme-toggle-sidebar-selected-background: var(--color-theme-toggle-sidebar-selected-background);\n  --color-ai-chat-composer-add-background:       var(--color-ai-chat-composer-add-background);\n  --color-ai-chat-composer-add-hover-background: var(--color-ai-chat-composer-add-hover-background);\n  --color-agent-progress-ring:                     var(--color-agent-progress-ring);\n  --color-button-ghost-background: transparent;\n  --color-button-ghost-hover: var(--color-background-primary-hover);\n  --color-button-ghost-active: var(--color-background-primary-active);\n  --color-button-ghost-disabled: transparent;\n  --color-button-ghost-foreground: var(--color-text-primary);\n  --color-button-ghost-disabled-foreground: var(--color-text-tertiary);\n  --color-dropdown-item-hover-background:        var(--color-dropdown-item-hover-background);\n  --color-kbd-background:                        var(--color-kbd-background);\n  --color-kbd-foreground:                        var(--color-kbd-foreground);\n  --color-docs-file-chip-background:             var(--color-docs-file-chip-background);\n  --color-docs-file-chip-border:                 var(--color-docs-file-chip-border);\n  --color-docs-file-chip-foreground:             var(--color-docs-file-chip-foreground);\n  --color-docs-command-accent:                    var(--color-docs-command-accent);\n  --color-docs-code-background:                   var(--color-docs-code-background);\n  --color-docs-install-rail:                      var(--color-docs-install-rail);\n  --color-badge-neutral-background: var(--color-badge-neutral-background);\n  --color-team-menu-count-background: var(--color-team-menu-count-background);\n  --color-team-menu-count-foreground: var(--color-team-menu-count-foreground);\n  --color-badge-new-background: var(--color-badge-new-background);\n  --color-badge-new-text:       var(--color-badge-new-text);\n  --color-tab-count-selected-background: var(--color-tab-count-selected-background);\n  --color-pill-tab-blue-selected-background: var(--color-pill-tab-blue-selected-background);\n  --color-pill-tab-blue-hover-background: var(--color-pill-tab-blue-hover-background);\n  --color-notification-information-background: var(--color-notification-information-background);\n  --color-notification-information-foreground: var(--color-notification-information-foreground);\n  --color-notification-success-background:     var(--color-notification-success-background);\n  --color-notification-success-foreground:     var(--color-notification-success-foreground);\n  --color-notification-error-background:       var(--color-notification-error-background);\n  --color-notification-error-foreground:       var(--color-notification-error-foreground);\n  --color-notification-center-background:      var(--color-notification-center-background);\n  --color-status-lime-background:   var(--color-status-lime-background);\n  --color-status-lime-text:         var(--color-status-lime-text);\n  --color-status-yellow-background: var(--color-status-yellow-background);\n  --color-status-yellow-text:       var(--color-status-yellow-text);\n  --color-status-rose-background:   var(--color-status-rose-background);\n  --color-status-rose-text:         var(--color-status-rose-text);\n  --color-status-cyan-background:   var(--color-status-cyan-background);\n  --color-status-cyan-text:         var(--color-status-cyan-text);\n  --color-status-blue-background:   var(--color-status-blue-background);\n  --color-status-blue-text:         var(--color-status-blue-text);\n  --color-status-purple-background: var(--color-status-purple-background);\n  --color-status-purple-text:       var(--color-status-purple-text);\n  --color-status-dot-green-halo:    var(--color-status-dot-green-halo);\n  --color-status-dot-yellow-halo:   var(--color-status-dot-yellow-halo);\n  --color-status-dot-indigo-halo:   var(--color-status-dot-indigo-halo);\n  --color-date-range-background:      var(--color-date-range-background);\n  --color-date-range-edge-background: var(--color-date-range-edge-background);\n  --color-calendar-event-blue-background:    var(--color-calendar-event-blue-background);\n  --color-calendar-event-blue-title:         var(--color-calendar-event-blue-title);\n  --color-calendar-event-blue-time:          var(--color-calendar-event-blue-time);\n  --color-calendar-event-pink-background:    var(--color-calendar-event-pink-background);\n  --color-calendar-event-pink-title:         var(--color-calendar-event-pink-title);\n  --color-calendar-event-pink-time:          var(--color-calendar-event-pink-time);\n  --color-calendar-event-purple-background:  var(--color-calendar-event-purple-background);\n  --color-calendar-event-purple-title:       var(--color-calendar-event-purple-title);\n  --color-calendar-event-purple-time:        var(--color-calendar-event-purple-time);\n  --color-calendar-event-lime-background:    var(--color-calendar-event-lime-background);\n  --color-calendar-event-lime-title:         var(--color-calendar-event-lime-title);\n  --color-calendar-event-lime-time:          var(--color-calendar-event-lime-time);\n  --color-calendar-event-emerald-background: var(--color-calendar-event-emerald-background);\n  --color-calendar-event-emerald-title:      var(--color-calendar-event-emerald-title);\n  --color-calendar-event-emerald-time:       var(--color-calendar-event-emerald-time);\n\n  --color-text-error-placeholder: var(--color-text-error-placeholder);\n\n  --color-chart-track:    var(--color-chart-track);\n  --color-chart-cursor:   var(--color-chart-cursor);\n  --color-chart-neutral:  var(--color-chart-neutral);\n  --color-chart-1:        var(--color-chart-1);\n  --color-chart-1-active: var(--color-chart-1-active);\n  --color-chart-2:        var(--color-chart-2);\n  --color-chart-2-active: var(--color-chart-2-active);\n  --color-chart-3:        var(--color-chart-3);\n  --color-chart-3-active: var(--color-chart-3-active);\n  --color-chart-4:        var(--color-chart-4);\n  --color-chart-4-active: var(--color-chart-4-active);\n  --color-chart-5:        var(--color-chart-5);\n  --color-chart-5-active: var(--color-chart-5-active);\n  --color-chart-6:        var(--color-chart-6);\n  --color-chart-6-active: var(--color-chart-6-active);\n  --color-chart-7:        var(--color-chart-7);\n  --color-chart-7-active: var(--color-chart-7-active);\n  --color-chart-8:        var(--color-chart-8);\n  --color-chart-8-active: var(--color-chart-8-active);\n  --color-chart-9:        var(--color-chart-9);\n  --color-chart-9-active: var(--color-chart-9-active);\n  --color-chart-agents-bar:        var(--color-chart-agents-bar);\n  --color-chart-agents-bar-active: var(--color-chart-agents-bar-active);\n}\n\n/* ---------------------------------------------------------------------------\n * Component utilities — Buttons\n *\n * Each variant collapses the four interactive states (default / hover / active\n * / disabled) into a single class. Use them on the component:\n *\n *   <button class=\"bg-button-primary\">…</button>\n *   <button class=\"bg-button-danger\">…</button>\n *\n * Smooth default→hover transition trick:\n *   CSS can't interpolate `background-image`, so swapping gradients on :hover\n *   would snap. Instead we keep the *default* gradient on the element and\n *   layer the *hover* gradient on a `::before` pseudo-element whose opacity\n *   fades 0→1 over `--button-transition-ms`. Active/disabled gradients still\n *   swap instantly (we hide the pseudo to reveal them).\n *\n * Stacking: `isolation: isolate` keeps the pseudo behind the button's text\n * and icons (z-index -1) while staying above the element's own background.\n * `border-radius: inherit` + `overflow: hidden` keeps it within the rounded\n * corners.\n * --------------------------------------------------------------------------- */\n@theme {\n  --button-transition-ms: 150ms;\n  --input-transition-ms:  150ms;\n}\n\n/* Keep color feedback quick while the physical press motion remains soft.\n * The transform uses a shorter duration on press and the slower duration only\n * when returning to rest, so hover styles never inherit the 700ms timing. */\n@utility button-press-motion {\n  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, opacity 150ms ease;\n  @media (prefers-reduced-motion: reduce) { transition: none; }\n}\n\n@utility bg-button-primary {\n  background: var(--color-text-primary);\n  color: var(--color-background-primary-default);\n  &:hover:not(:disabled):not([aria-disabled=\"true\"]) { opacity: .88; }\n  &:active:not(:disabled):not([aria-disabled=\"true\"]) { opacity: .78; }\n  &:disabled, &[aria-disabled=\"true\"] { background: var(--color-background-primary-disabled); color: var(--color-text-tertiary); }\n}\n\n@utility bg-button-danger {\n  position: relative;\n  isolation: isolate;\n  background-image: var(--gradient-button-danger-default);\n\n  &::before {\n    content: \"\";\n    position: absolute;\n    inset: 0;\n    z-index: -1;\n    pointer-events: none;\n    border-radius: inherit;\n    background-image: var(--gradient-button-danger-hover);\n    opacity: 0;\n    transition: opacity var(--button-transition-ms) ease;\n  }\n  &:hover:not(:disabled):not([aria-disabled=\"true\"])::before {\n    opacity: 1;\n  }\n  &:active:not(:disabled):not([aria-disabled=\"true\"]) {\n    background-image: var(--gradient-button-danger-active);\n  }\n  &:active:not(:disabled):not([aria-disabled=\"true\"])::before {\n    opacity: 0;\n  }\n  &:disabled,\n  &[aria-disabled=\"true\"] {\n    background-image: var(--gradient-button-danger-disabled);\n  }\n  &:disabled::before,\n  &[aria-disabled=\"true\"]::before {\n    display: none;\n  }\n}\n"
    },
    {
      "path": "components/boardui/styles/typography.css",
      "target": "@components/boardui/styles/typography.css",
      "type": "registry:file",
      "content": "/* ---------------------------------------------------------------------------\n * BoardUI typography\n *\n * Type ramp from Figma → text styles. Each style registers a Tailwind v4\n * text utility — `text-{name}` applies font-size + line-height + letter-spacing\n * + font-weight in one go.\n *\n * Family: Inter for the whole ramp. (The Figma export marks Large Title /\n * Regular as \"Inter Display\"; we map everything to Inter for consistency since\n * Inter ships a variable optical-size axis. Flag if a true Inter Display load\n * is needed.)\n *\n * Weight mapping (mirrors Figma variables — font/weight/*):\n *   Regular   = 400\n *   Medium    = 500\n *   Semibold  = 600  (Figma variable font/weight/semibold = 600)\n *   Bold      = 700\n *\n * Naming: `<family>-<weight>` in lowercase kebab — e.g. `text-body-medium`,\n * `text-title-1-semibold`, `text-caption-2-bold`.\n * --------------------------------------------------------------------------- */\n\n@theme {\n  /* ===== Large Title — 64 / 80 ============================================= */\n  --text-large-title-regular: 4rem;\n  --text-large-title-regular--line-height: 5rem;\n  --text-large-title-regular--letter-spacing: 0;\n  --text-large-title-regular--font-weight: 400;\n  --text-large-title-medium: 4rem;\n  --text-large-title-medium--line-height: 5rem;\n  --text-large-title-medium--letter-spacing: 0;\n  --text-large-title-medium--font-weight: 500;\n  --text-large-title-semibold: 4rem;\n  --text-large-title-semibold--line-height: 5rem;\n  --text-large-title-semibold--letter-spacing: 0;\n  --text-large-title-semibold--font-weight: 600;\n  --text-large-title-bold: 4rem;\n  --text-large-title-bold--line-height: 5rem;\n  --text-large-title-bold--letter-spacing: 0;\n  --text-large-title-bold--font-weight: 700;\n\n  /* ===== Display 1 — 56 / 72 =============================================== */\n  --text-display-1-regular: 3.5rem;\n  --text-display-1-regular--line-height: 4.5rem;\n  --text-display-1-regular--letter-spacing: 0;\n  --text-display-1-regular--font-weight: 400;\n  --text-display-1-medium: 3.5rem;\n  --text-display-1-medium--line-height: 4.5rem;\n  --text-display-1-medium--letter-spacing: 0;\n  --text-display-1-medium--font-weight: 500;\n  --text-display-1-semibold: 3.5rem;\n  --text-display-1-semibold--line-height: 4.5rem;\n  --text-display-1-semibold--letter-spacing: 0;\n  --text-display-1-semibold--font-weight: 600;\n  --text-display-1-bold: 3.5rem;\n  --text-display-1-bold--line-height: 4.5rem;\n  --text-display-1-bold--letter-spacing: 0;\n  --text-display-1-bold--font-weight: 700;\n\n  /* ===== Display 2 — 48 / 64 =============================================== */\n  --text-display-2-regular: 3rem;\n  --text-display-2-regular--line-height: 4rem;\n  --text-display-2-regular--letter-spacing: 0;\n  --text-display-2-regular--font-weight: 400;\n  --text-display-2-medium: 3rem;\n  --text-display-2-medium--line-height: 4rem;\n  --text-display-2-medium--letter-spacing: 0;\n  --text-display-2-medium--font-weight: 500;\n  --text-display-2-semibold: 3rem;\n  --text-display-2-semibold--line-height: 4rem;\n  --text-display-2-semibold--letter-spacing: 0;\n  --text-display-2-semibold--font-weight: 600;\n  --text-display-2-bold: 3rem;\n  --text-display-2-bold--line-height: 4rem;\n  --text-display-2-bold--letter-spacing: 0;\n  --text-display-2-bold--font-weight: 700;\n\n  /* ===== Display 3 — 40 / 54 =============================================== */\n  --text-display-3-regular: 2.5rem;\n  --text-display-3-regular--line-height: 3.375rem;\n  --text-display-3-regular--letter-spacing: 0;\n  --text-display-3-regular--font-weight: 400;\n  --text-display-3-medium: 2.5rem;\n  --text-display-3-medium--line-height: 3.375rem;\n  --text-display-3-medium--letter-spacing: 0;\n  --text-display-3-medium--font-weight: 500;\n  --text-display-3-semibold: 2.5rem;\n  --text-display-3-semibold--line-height: 3.375rem;\n  --text-display-3-semibold--letter-spacing: 0;\n  --text-display-3-semibold--font-weight: 600;\n  --text-display-3-bold: 2.5rem;\n  --text-display-3-bold--line-height: 3.375rem;\n  --text-display-3-bold--letter-spacing: 0;\n  --text-display-3-bold--font-weight: 700;\n\n  /* ===== Display 4 — 32 / 44 =============================================== */\n  --text-display-4-regular: 2rem;\n  --text-display-4-regular--line-height: 2.75rem;\n  --text-display-4-regular--letter-spacing: 0;\n  --text-display-4-regular--font-weight: 400;\n  --text-display-4-medium: 2rem;\n  --text-display-4-medium--line-height: 2.75rem;\n  --text-display-4-medium--letter-spacing: 0;\n  --text-display-4-medium--font-weight: 500;\n  --text-display-4-semibold: 2rem;\n  --text-display-4-semibold--line-height: 2.75rem;\n  --text-display-4-semibold--letter-spacing: 0;\n  --text-display-4-semibold--font-weight: 600;\n  --text-display-4-bold: 2rem;\n  --text-display-4-bold--line-height: 2.75rem;\n  --text-display-4-bold--letter-spacing: 0;\n  --text-display-4-bold--font-weight: 700;\n\n  /* ===== Title 1 — 24 / 34 ================================================= */\n  --text-title-1-regular: 1.5rem;\n  --text-title-1-regular--line-height: 2.125rem;\n  --text-title-1-regular--letter-spacing: 0;\n  --text-title-1-regular--font-weight: 400;\n  --text-title-1-medium: 1.5rem;\n  --text-title-1-medium--line-height: 2.125rem;\n  --text-title-1-medium--letter-spacing: 0;\n  --text-title-1-medium--font-weight: 500;\n  --text-title-1-semibold: 1.5rem;\n  --text-title-1-semibold--line-height: 2.125rem;\n  --text-title-1-semibold--letter-spacing: 0;\n  --text-title-1-semibold--font-weight: 600;\n  --text-title-1-bold: 1.5rem;\n  --text-title-1-bold--line-height: 2.125rem;\n  --text-title-1-bold--letter-spacing: 0;\n  --text-title-1-bold--font-weight: 700;\n\n  /* ===== Title 2 — 20 / 26 ================================================= */\n  --text-title-2-regular: 1.25rem;\n  --text-title-2-regular--line-height: 1.625rem;\n  --text-title-2-regular--letter-spacing: 0;\n  --text-title-2-regular--font-weight: 400;\n  --text-title-2-medium: 1.25rem;\n  --text-title-2-medium--line-height: 1.625rem;\n  --text-title-2-medium--letter-spacing: 0;\n  --text-title-2-medium--font-weight: 500;\n  --text-title-2-semibold: 1.25rem;\n  --text-title-2-semibold--line-height: 1.625rem;\n  --text-title-2-semibold--letter-spacing: 0;\n  --text-title-2-semibold--font-weight: 600;\n  --text-title-2-bold: 1.25rem;\n  --text-title-2-bold--line-height: 1.625rem;\n  --text-title-2-bold--letter-spacing: 0;\n  --text-title-2-bold--font-weight: 700;\n\n  /* ===== Title 3 — 18 / 24-26 ============================================== */\n  --text-title-3-regular: 1.125rem;\n  --text-title-3-regular--line-height: 1.5rem;     /* 24 per Figma */\n  --text-title-3-regular--letter-spacing: 0;\n  --text-title-3-regular--font-weight: 400;\n  --text-title-3-medium: 1.125rem;\n  --text-title-3-medium--line-height: 1.625rem;    /* 26 per Figma */\n  --text-title-3-medium--letter-spacing: 0;\n  --text-title-3-medium--font-weight: 500;\n  --text-title-3-semibold: 1.125rem;\n  --text-title-3-semibold--line-height: 1.625rem;\n  --text-title-3-semibold--letter-spacing: 0;\n  --text-title-3-semibold--font-weight: 600;\n  --text-title-3-bold: 1.125rem;\n  --text-title-3-bold--line-height: 1.625rem;\n  --text-title-3-bold--letter-spacing: 0;\n  --text-title-3-bold--font-weight: 700;\n\n  /* ===== Headline — 16 / 22 ================================================ */\n  --text-headline-regular: 1rem;\n  --text-headline-regular--line-height: 1.375rem;\n  --text-headline-regular--letter-spacing: 0;\n  --text-headline-regular--font-weight: 400;\n  --text-headline-medium: 1rem;\n  --text-headline-medium--line-height: 1.375rem;\n  --text-headline-medium--letter-spacing: 0;\n  --text-headline-medium--font-weight: 500;\n  --text-headline-semibold: 1rem;\n  --text-headline-semibold--line-height: 1.375rem;\n  --text-headline-semibold--letter-spacing: 0;\n  --text-headline-semibold--font-weight: 600;\n  --text-headline-bold: 1rem;\n  --text-headline-bold--line-height: 1.375rem;\n  --text-headline-bold--letter-spacing: 0;\n  --text-headline-bold--font-weight: 700;\n\n  /* ===== Body — 14 / 20 ==================================================== */\n  --text-body-regular: 0.875rem;\n  --text-body-regular--line-height: 1.25rem;\n  --text-body-regular--letter-spacing: 0;\n  --text-body-regular--font-weight: 400;\n  --text-body-medium: 0.875rem;\n  --text-body-medium--line-height: 1.25rem;\n  --text-body-medium--letter-spacing: 0;\n  --text-body-medium--font-weight: 500;\n  --text-body-semibold: 0.875rem;\n  --text-body-semibold--line-height: 1.25rem;\n  --text-body-semibold--letter-spacing: 0;\n  --text-body-semibold--font-weight: 600;\n  --text-body-bold: 0.875rem;\n  --text-body-bold--line-height: 1.25rem;\n  --text-body-bold--letter-spacing: 0;\n  --text-body-bold--font-weight: 700;\n\n  /* ===== Body 2 — 13 / 18 ================================================== */\n  --text-body-2-regular: 0.8125rem;\n  --text-body-2-regular--line-height: 1.125rem;\n  --text-body-2-regular--letter-spacing: 0;\n  --text-body-2-regular--font-weight: 400;\n  --text-body-2-medium: 0.8125rem;\n  --text-body-2-medium--line-height: 1.125rem;\n  --text-body-2-medium--letter-spacing: 0;\n  --text-body-2-medium--font-weight: 500;\n  --text-body-2-semibold: 0.8125rem;\n  --text-body-2-semibold--line-height: 1.125rem;\n  --text-body-2-semibold--letter-spacing: 0;\n  --text-body-2-semibold--font-weight: 600;\n  --text-body-2-bold: 0.8125rem;\n  --text-body-2-bold--line-height: 1.125rem;\n  --text-body-2-bold--letter-spacing: 0;\n  --text-body-2-bold--font-weight: 700;\n\n  /* ===== Caption 1 — 12 / 16 (tracking 0.15) =============================== */\n  --text-caption-1-regular: 0.75rem;\n  --text-caption-1-regular--line-height: 1rem;\n  --text-caption-1-regular--letter-spacing: 0.15px;\n  --text-caption-1-regular--font-weight: 400;\n  --text-caption-1-medium: 0.75rem;\n  --text-caption-1-medium--line-height: 1rem;\n  --text-caption-1-medium--letter-spacing: 0.15px;\n  --text-caption-1-medium--font-weight: 500;\n  --text-caption-1-semibold: 0.75rem;\n  --text-caption-1-semibold--line-height: 1rem;\n  --text-caption-1-semibold--letter-spacing: 0.15px;\n  --text-caption-1-semibold--font-weight: 600;\n  --text-caption-1-bold: 0.75rem;\n  --text-caption-1-bold--line-height: 1rem;\n  --text-caption-1-bold--letter-spacing: 0.15px;\n  --text-caption-1-bold--font-weight: 700;\n\n  /* ===== Caption 2 — 11 / 15 (tracking 0.2) ================================ */\n  --text-caption-2-regular: 0.6875rem;\n  --text-caption-2-regular--line-height: 0.9375rem;\n  --text-caption-2-regular--letter-spacing: 0.2px;\n  --text-caption-2-regular--font-weight: 400;\n  --text-caption-2-medium: 0.6875rem;\n  --text-caption-2-medium--line-height: 0.9375rem;\n  --text-caption-2-medium--letter-spacing: 0.2px;\n  --text-caption-2-medium--font-weight: 500;\n  --text-caption-2-semibold: 0.6875rem;\n  --text-caption-2-semibold--line-height: 0.9375rem;\n  --text-caption-2-semibold--letter-spacing: 0.2px;\n  --text-caption-2-semibold--font-weight: 600;\n  --text-caption-2-bold: 0.6875rem;\n  --text-caption-2-bold--line-height: 0.9375rem;\n  --text-caption-2-bold--letter-spacing: 0.2px;\n  --text-caption-2-bold--font-weight: 700;\n}\n"
    },
    {
      "path": "components/boardui/SOURCE.json",
      "target": "@components/boardui/SOURCE.json",
      "type": "registry:file",
      "content": "{\n  \"source\": \"https://github.com/BoardUI/boardui\",\n  \"revision\": \"3e76e282614b147a34b9b2a510e31b97d58a3909\",\n  \"license\": \"MIT\",\n  \"scope\": [\n    \"components/application/agent-chat\",\n    \"components/application/agent-thinking\",\n    \"components/application/composer-loader\",\n    \"components/base\",\n    \"components/foundations\",\n    \"styles\",\n    \"utils\"\n  ],\n  \"adaptations\": [\n    \"Repository import paths point to components/boardui\",\n    \"Neutral primary and ghost actions, restrained feedback, and readable agent surface tokens\",\n    \"Focusable native actions support React Aria tooltip composition\",\n    \"Icon-only child SVG fallback preserves explicit leadingIcon precedence\",\n    \"The Agent Chat starter always uses the bundled local demo transport; API probing, provider keys, dashboard navigation, and Pro promotion are omitted\",\n    \"Agent Chat imports resolve through components/boardui and its account logout action closes the local menu instead of navigating to a starter-only route\",\n    \"The gallery chat uses a responsive history drawer below xl; selecting a thread, clicking outside, or pressing Escape closes it\",\n    \"The prior pinned base components are unchanged at the current revision\"\n  ]\n}\n"
    },
    {
      "path": "components/beautiful-ui/SOURCE.json",
      "target": "@components/beautiful-ui/SOURCE.json",
      "type": "registry:file",
      "content": "{\n  \"name\": \"Beautiful UI\",\n  \"repository\": \"https://github.com/slev12397/beautiful-ui\",\n  \"revision\": \"ff0f74d62d8be9d89bcb735b3632e31a6ccf88dc\",\n  \"license\": \"MIT\",\n  \"copyright\": \"Copyright (c) 2026 Shane Levine\",\n  \"families\": 21,\n  \"adaptation\": \"Original primitives and internal building blocks retain their APIs and interactions. The gallery uses Agents Kit fixtures; Beautiful UI theme utilities compile through the main Tailwind stylesheet. Shared semantic colors, readable tertiary text, neutral focus, and Inter/JetBrains Mono typography follow the Agents Kit design contract. Proprietary Iconists icons use Lucide equivalents. Existing root adapters remain available. Selection Actions uses a contained two-row toolbar on narrow hosts, while retaining the desktop floating toolbar.\"\n}\n"
    },
    {
      "path": "components/voice-agents/livekit/LICENSE",
      "target": "@components/voice-agents/livekit/LICENSE",
      "type": "registry:file",
      "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
    },
    {
      "path": "components/voice-agents/livekit/SOURCE.json",
      "target": "@components/voice-agents/livekit/SOURCE.json",
      "type": "registry:file",
      "content": "{\n  \"name\": \"LiveKit Agents UI\",\n  \"repository\": \"https://github.com/livekit/components-js\",\n  \"revision\": \"20aa613fcab699385fe7385d0bf31a9262210421\",\n  \"license\": \"Apache-2.0 with an MIT file-level exception\",\n  \"sourceRoot\": \"packages/shadcn\",\n  \"components\": [\n    {\n      \"name\": \"agent-disconnect-button\",\n      \"path\": \"agent-disconnect-button.tsx\",\n      \"title\": \"Agent Disconnect Button\"\n    },\n    {\n      \"name\": \"agent-track-toggle\",\n      \"path\": \"agent-track-toggle.tsx\",\n      \"title\": \"Agent Track Toggle\"\n    },\n    {\n      \"name\": \"agent-track-control\",\n      \"path\": \"agent-track-control.tsx\",\n      \"title\": \"Agent Track Control\"\n    },\n    {\n      \"name\": \"agent-control-bar\",\n      \"path\": \"agent-control-bar.tsx\",\n      \"title\": \"Agent Control Bar\"\n    },\n    {\n      \"name\": \"agent-audio-visualizer-bar\",\n      \"path\": \"agent-audio-visualizer-bar.tsx\",\n      \"title\": \"Agent Audio Visualizer Bar\"\n    },\n    {\n      \"name\": \"agent-audio-visualizer-radial\",\n      \"path\": \"agent-audio-visualizer-radial.tsx\",\n      \"title\": \"Agent Audio Visualizer Radial\"\n    },\n    {\n      \"name\": \"agent-audio-visualizer-grid\",\n      \"path\": \"agent-audio-visualizer-grid.tsx\",\n      \"title\": \"Agent Audio Visualizer Grid\"\n    },\n    {\n      \"name\": \"agent-session-provider\",\n      \"path\": \"agent-session-provider.tsx\",\n      \"title\": \"Agent Session Provider\"\n    },\n    {\n      \"name\": \"start-audio-button\",\n      \"path\": \"start-audio-button.tsx\",\n      \"title\": \"Start Audio Button\"\n    },\n    {\n      \"name\": \"agent-chat-indicator\",\n      \"path\": \"agent-chat-indicator.tsx\",\n      \"title\": \"Agent Chat Indicator\"\n    },\n    {\n      \"name\": \"agent-chat-transcript\",\n      \"path\": \"agent-chat-transcript.tsx\",\n      \"title\": \"Agent Chat Transcript\"\n    },\n    {\n      \"name\": \"react-shader-toy\",\n      \"path\": \"react-shader-toy.tsx\",\n      \"title\": \"React Shader Toy\"\n    },\n    {\n      \"name\": \"agent-audio-visualizer-wave\",\n      \"path\": \"agent-audio-visualizer-wave.tsx\",\n      \"title\": \"Agent Audio Visualizer Wave\"\n    },\n    {\n      \"name\": \"agent-session-view-01\",\n      \"path\": \"blocks/agent-session-view-01/components/agent-session-block.tsx\",\n      \"title\": \"Agent Session View\"\n    },\n    {\n      \"name\": \"agent-popup-01\",\n      \"path\": \"blocks/agent-popup-01/components/agent-popup-block.tsx\",\n      \"title\": \"Agent Popup\"\n    }\n  ],\n  \"changes\": [\n    \"Rewritten local imports into components/voice-agents/livekit.\",\n    \"Bundled the registry UI dependency closure locally; MessageScroller uses an equivalent native scroll implementation because @shadcn/react is not a project dependency.\",\n    \"ReactShaderToy caps device pixel ratio at 2, pauses while off-screen, and renders a static frame when reduced motion is requested.\",\n    \"Bar, radial, grid, wave, and chat-indicator animation loops stop or resolve to a static state when reduced motion is requested.\",\n    \"Gallery previews use explicit offline state and volume overrides and never create a fake LiveKit room or start audio capture.\",\n    \"Marked hook-based public entry points as client components for direct Next.js App Router imports.\",\n    \"Replaced the popup trigger robot glyph with a contextual Lucide audio-lines icon.\"\n  ],\n  \"fileLevelLicenses\": {\n    \"react-shader-toy.tsx\": \"MIT\"\n  },\n  \"excluded\": [\n    {\n      \"name\": \"agent-audio-visualizer-aura\",\n      \"reason\": \"Upstream file is licensed under PolyForm Non-Resale 1.0.0, so it is not included in this distributable kit.\"\n    }\n  ]\n}\n"
    },
    {
      "path": "components/voice-agents/livekit/NOTICE",
      "target": "@components/voice-agents/livekit/NOTICE",
      "type": "registry:file",
      "content": "Copyright 2023 LiveKit, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\nThis product includes code from react-shaders (https://github.com/rysana-ai/react-shaders)\n\n   Copyright (c) 2018 Morgan Villedieu\n   Copyright (c) 2023 Rysana, Inc. (forked from the above)\n\nLicensed under the MIT License\n"
    },
    {
      "path": "components/voice-agents/orbkit/LICENSE",
      "target": "@components/voice-agents/orbkit/LICENSE",
      "type": "registry:file",
      "content": "MIT License\n\nCopyright (c) 2026 zzzzshawn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
    },
    {
      "path": "components/voice-agents/orbkit/SOURCE.json",
      "target": "@components/voice-agents/orbkit/SOURCE.json",
      "type": "registry:file",
      "content": "{\n  \"name\": \"OrbKit\",\n  \"repository\": \"https://github.com/zzzzshawn/orbkit\",\n  \"commit\": \"35e42484560fd35e8502703ba58fa99541d8c686\",\n  \"license\": \"MIT\",\n  \"copyright\": \"Copyright (c) 2026 zzzzshawn\",\n  \"core\": \"components/voice-agents/orbkit/core.tsx\",\n  \"credits\": \"components/voice-agents/orbkit/CREDITS.md\",\n  \"shaderLicense\": \"components/voice-agents/orbkit/LICENSE-SHADERS.md\",\n  \"changes\": [\n    \"Moved the dependency-free OrbKit runtime and selected variants into the Agents Kit voice collection.\",\n    \"Mapped variant imports to the local shared runtime.\",\n    \"Kept the upstream reduced-motion, offscreen pause, DPR cap, context recovery, and cleanup behavior.\"\n  ],\n  \"exclusions\": [\n    {\n      \"name\": \"shdr-19\",\n      \"reason\": \"Non-commercial XorDev shader; excluded from this distributable MIT collection.\",\n      \"license\": \"components/voice-agents/orbkit/LICENSE-SHADERS.md\"\n    },\n    {\n      \"name\": \"XorDev shader ports\",\n      \"reason\": \"All variants listed under Shaders by XorDev in CREDITS.md are non-commercial and excluded.\",\n      \"license\": \"components/voice-agents/orbkit/LICENSE-SHADERS.md\"\n    }\n  ],\n  \"components\": [\n    {\n      \"name\": \"shdr-11\",\n      \"path\": \"shdr-11.tsx\",\n      \"title\": \"Hydrogen\"\n    },\n    {\n      \"name\": \"shdr-13\",\n      \"path\": \"shdr-13.tsx\",\n      \"title\": \"Ion\"\n    },\n    {\n      \"name\": \"shdr-14\",\n      \"path\": \"shdr-14.tsx\",\n      \"title\": \"Dither\"\n    },\n    {\n      \"name\": \"shdr-21\",\n      \"path\": \"shdr-21.tsx\",\n      \"title\": \"Nimbus\"\n    }\n  ]\n}\n"
    },
    {
      "path": "components/voice-agents/orbkit/CREDITS.md",
      "target": "@components/voice-agents/orbkit/CREDITS.md",
      "type": "registry:file",
      "content": "# Credits\n\nOrbkit ships two kinds of orb. Every file says which it is.\n\n## Shaders by XorDev\n\nThese 19 orbs are ported from golfed shaders by [XorDev](https://x.com/XorDev),\nused with his permission (agreed by direct message on 2026-09-06). Terms:\n**non-commercial use only, with attribution to XorDev**. Each file starts with\na notice that must stay with any copy, and the registry marks these items with\na `credit` field. See [LICENSE-SHADERS.md](LICENSE-SHADERS.md).\n\n| Orb     | Shader by | Original             |\n| ------- | --------- | -------------------- |\n| shdr-01 | XorDev    | https://x.com/XorDev |\n| shdr-02 | XorDev    | https://x.com/XorDev |\n| shdr-03 | XorDev    | https://x.com/XorDev |\n| shdr-04 | XorDev    | https://x.com/XorDev |\n| shdr-05 | XorDev    | https://x.com/XorDev |\n| shdr-06 | XorDev    | https://x.com/XorDev |\n| shdr-07 | XorDev    | https://x.com/XorDev |\n| shdr-08 | XorDev    | https://x.com/XorDev |\n| shdr-09 | XorDev    | https://x.com/XorDev |\n| shdr-10 | XorDev    | https://x.com/XorDev |\n| shdr-15 | XorDev    | https://x.com/XorDev |\n| shdr-18 | XorDev    | https://x.com/XorDev |\n| shdr-19 | XorDev    | https://x.com/XorDev |\n| shdr-20 | XorDev    | https://x.com/XorDev |\n| shdr-22 | XorDev    | https://x.com/XorDev |\n| shdr-25 | XorDev    | https://x.com/XorDev |\n| shdr-26 | XorDev    | https://x.com/XorDev |\n| shdr-28 | XorDev    | https://x.com/XorDev |\n| shdr-31 | XorDev    | https://x.com/XorDev |\n\nReplace a profile link with the specific post once it is known.\n\n## Original orbs\n\nThese 14 orbs are Orbkit's own work and are MIT-licensed, like the runtime:\nshdr-11, shdr-12, shdr-13, shdr-14, shdr-16, shdr-17, shdr-21, shdr-23, shdr-24, shdr-27, shdr-29, shdr-30, shdr-32, shdr-33.\n\n## Adding shaders\n\nAsk the original author before porting anything that is not your own. XorDev\nasked to be consulted before any more of his work is included.\n"
    },
    {
      "path": "components/voice-agents/orbkit/LICENSE-SHADERS.md",
      "target": "@components/voice-agents/orbkit/LICENSE-SHADERS.md",
      "type": "registry:file",
      "content": "# Licence for the ported shaders\n\nThe orb files listed under \"Shaders by XorDev\" in [CREDITS.md](CREDITS.md) are\nderived from shaders by XorDev (https://x.com/XorDev) and are used with the\nauthor's permission on these terms:\n\n1. Non-commercial use only.\n2. Attribution to XorDev must be kept with every copy, including the notice at\n   the top of each file.\n3. Any further use of XorDev's work requires asking the author first.\n\nEverything else in this repository, including the runtime\n(`orbkit-core.tsx`), the site, the build scripts, and the original orbs, is\nlicensed under the MIT licence in [LICENSE](LICENSE).\n"
    },
    {
      "path": "components/prompt-kit/LICENSE.md",
      "target": "@components/prompt-kit/LICENSE.md",
      "type": "registry:file",
      "content": "MIT License\n\nCopyright (c) 2025 Julien Thibeaut\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. app/page.tsx\n"
    },
    {
      "path": "components/prompt-kit/SOURCE.json",
      "target": "@components/prompt-kit/SOURCE.json",
      "type": "registry:file",
      "content": "{\n  \"name\": \"Prompt Kit\",\n  \"upstream\": \"https://github.com/ibelick/prompt-kit\",\n  \"revision\": \"de80375967400aa0c6ebab9d3ba4f9258ab79fcc\",\n  \"license\": \"MIT\",\n  \"sourceFiles\": [\n    \"components/ui/hover-card.tsx\",\n    \"components/prompt-kit/chain-of-thought.tsx\",\n    \"components/prompt-kit/chat-container.tsx\",\n    \"components/prompt-kit/code-block.tsx\",\n    \"components/prompt-kit/feedback-bar.tsx\",\n    \"components/prompt-kit/file-upload.tsx\",\n    \"components/prompt-kit/image.tsx\",\n    \"components/prompt-kit/jsx-preview.tsx\",\n    \"components/prompt-kit/loader.tsx\",\n    \"components/prompt-kit/markdown.tsx\",\n    \"components/prompt-kit/message.tsx\",\n    \"components/prompt-kit/prompt-input.tsx\",\n    \"components/prompt-kit/prompt-suggestion.tsx\",\n    \"components/prompt-kit/reasoning.tsx\",\n    \"components/prompt-kit/response-stream.tsx\",\n    \"components/prompt-kit/scroll-button.tsx\",\n    \"components/prompt-kit/source.tsx\",\n    \"components/prompt-kit/steps.tsx\",\n    \"components/prompt-kit/system-message.tsx\",\n    \"components/prompt-kit/text-shimmer.tsx\",\n    \"components/prompt-kit/thinking-bar.tsx\",\n    \"components/prompt-kit/tool.tsx\",\n    \"app/globals.css\"\n  ],\n  \"fullStackItems\": [\n    \"components/primitives/chatbot.tsx\",\n    \"components/primitives/tool-calling.tsx\"\n  ],\n  \"adaptation\": \"Public primitives retain upstream APIs, layout, states, and motion. PromptInput preserves the host IME guard; CodeBlock preserves data-code-theme while restoring upstream theme selection. Gallery examples use local fieldwork fixtures and no backend or API key. The gallery code themes follow the host theme unless explicitly selected; Markdown prose and status messages have readable dark-mode styling.\"\n}\n"
    },
    {
      "path": "components/prompt-kit/styles.css",
      "target": "@components/prompt-kit/styles.css",
      "type": "registry:file",
      "content": "@import \"../../styles/animations.css\";\n\n/* keyframes for loaders */\n@keyframes typing {\n  0%,\n  100% {\n    transform: translateY(0);\n    opacity: 0.5;\n  }\n  50% {\n    transform: translateY(-2px);\n    opacity: 1;\n  }\n}\n\n@keyframes loading-dots {\n  0%,\n  100% {\n    opacity: 0;\n  }\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes wave {\n  0%,\n  100% {\n    transform: scaleY(1);\n  }\n  50% {\n    transform: scaleY(0.6);\n  }\n}\n\n@keyframes blink {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0;\n  }\n}\n\n@keyframes text-blink {\n  0%,\n  100% {\n    color: var(--primary);\n  }\n  50% {\n    color: var(--muted-foreground);\n  }\n}\n\n@keyframes bounce-dots {\n  0%,\n  100% {\n    transform: scale(0.8);\n    opacity: 0.5;\n  }\n  50% {\n    transform: scale(1.2);\n    opacity: 1;\n  }\n}\n\n@keyframes thin-pulse {\n  0%,\n  100% {\n    transform: scale(0.95);\n    opacity: 0.8;\n  }\n  50% {\n    transform: scale(1.05);\n    opacity: 0.4;\n  }\n}\n\n@keyframes pulse-dot {\n  0%,\n  100% {\n    transform: scale(1);\n    opacity: 0.8;\n  }\n  50% {\n    transform: scale(1.5);\n    opacity: 1;\n  }\n}\n\n@keyframes shimmer-text {\n  0% {\n    background-position: 150% center;\n  }\n  100% {\n    background-position: -150% center;\n  }\n}\n\n@keyframes wave-bars {\n  0%,\n  100% {\n    transform: scaleY(1);\n    opacity: 0.5;\n  }\n  50% {\n    transform: scaleY(0.6);\n    opacity: 1;\n  }\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: 200% 50%;\n  }\n  100% {\n    background-position: -200% 50%;\n  }\n}\n\n@keyframes spinner-fade {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n"
    },
    {
      "path": "styles/animations.css",
      "target": "~/styles/animations.css",
      "type": "registry:file",
      "content": "@import \"tw-animate-css\";\n"
    }
  ],
  "categories": [
    "App examples"
  ]
}
