{
  "name": "ai-chat-default",
  "type": "registry:block",
  "tier": "free",
  "dependencies": [
    "motion",
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "https://www.craftui.space/r/craftui-base.json"
  ],
  "files": [
    {
      "path": "components/craftui/blocks/ai-chat/default.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  useReducedMotion,\n  type Transition,\n} from \"motion/react\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  Add01Icon,\n  Mic01Icon,\n  Camera01Icon,\n  Video01Icon,\n  File01Icon,\n  Attachment01Icon,\n  SparklesIcon,\n  ImageAdd01Icon,\n  Idea01Icon,\n  Telescope01Icon,\n  Globe02Icon,\n  ArrowUp01Icon,\n  ArrowDown01Icon,\n  Tick02Icon,\n  MessageQuestionIcon,\n  TaskDaily01Icon,\n  SourceCodeIcon,\n  NeuralNetworkIcon,\n  AiBrain01Icon,\n  CpuIcon,\n  AiMagicIcon,\n  Atom01Icon,\n} from \"@hugeicons/core-free-icons\";\n\nexport type HugeIcon = typeof SparklesIcon;\n\nexport interface ArcItem {\n  icon: HugeIcon;\n  label: string;\n  angle?: number;\n}\n\nexport interface ToolItem {\n  icon: HugeIcon;\n  label: string;\n}\n\nexport interface ChatModeOption {\n  id: string;\n  label: string;\n  icon: HugeIcon;\n  desc: string;\n}\n\nexport interface ModelOption {\n  id: string;\n  label: string;\n  tier: string;\n  color: string;\n  icon?: HugeIcon;\n}\n\nexport interface AIChatProps {\n  heading?: string;\n  placeholder?: string;\n\n  arcItems?: ArcItem[];\n  toolItems?: ToolItem[];\n  modes?: ChatModeOption[];\n  models?: ModelOption[];\n  mockPhrases?: string[];\n\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  onSubmit?: (value: string) => void;\n\n  defaultModeId?: string;\n  modeId?: string;\n  onModeChange?: (mode: ChatModeOption) => void;\n\n  defaultModelId?: string;\n  modelId?: string;\n  onModelChange?: (model: ModelOption) => void;\n\n  onToolChange?: (tool: ToolItem | null) => void;\n  onAttach?: (item: ArcItem) => void;\n  onRecordingChange?: (recording: boolean) => void;\n\n  className?: string;\n}\n\nconst EASE_OUT_QUART = [0.165, 0.84, 0.44, 1] as const;\nconst EASE_PANEL = [0.22, 1, 0.36, 1] as const;\nconst RADIUS = 80;\n\nconst EASE_OUT = [0.215, 0.61, 0.355, 1] as const;\nconst EXPAND_SPRING: Transition = {\n  type: \"spring\",\n  duration: 0.3,\n  bounce: 0.16,\n};\nconst COLLAPSE_SPRING: Transition = {\n  type: \"spring\",\n  duration: 0.22,\n  bounce: 0,\n};\nconst CONTENT_IN: Transition = { duration: 0.2, ease: EASE_OUT };\nconst CONTENT_OUT: Transition = { duration: 0.15, ease: EASE_OUT };\n\nconst DEFAULT_ARC_ITEMS: ArcItem[] = [\n  { icon: Camera01Icon, label: \"Photo\", angle: -55 },\n  { icon: Video01Icon, label: \"Video\", angle: -18 },\n  { icon: File01Icon, label: \"File\", angle: 18 },\n  { icon: Attachment01Icon, label: \"Attach\", angle: 55 },\n];\n\nconst DEFAULT_TOOL_ITEMS: ToolItem[] = [\n  { icon: ImageAdd01Icon, label: \"Create image\" },\n  { icon: Idea01Icon, label: \"Thinking\" },\n  { icon: Telescope01Icon, label: \"Deep research\" },\n  { icon: Globe02Icon, label: \"Web search\" },\n];\n\nconst DEFAULT_MODES: ChatModeOption[] = [\n  {\n    id: \"ask\",\n    label: \"Ask\",\n    icon: MessageQuestionIcon,\n    desc: \"Answer questions about anything\",\n  },\n  {\n    id: \"plan\",\n    label: \"Plan\",\n    icon: TaskDaily01Icon,\n    desc: \"Draft an approach before acting\",\n  },\n  {\n    id: \"build\",\n    label: \"Build\",\n    icon: SourceCodeIcon,\n    desc: \"Write and edit code end to end\",\n  },\n];\n\nconst DEFAULT_MODELS: ModelOption[] = [\n  {\n    id: \"kimi\",\n    label: \"Kimi K3\",\n    tier: \"High\",\n    color: \"oklch(0.21 0.034 264.665)\",\n    icon: NeuralNetworkIcon,\n  },\n  {\n    id: \"gpt-terra\",\n    label: \"GPT-5.6 Terra\",\n    tier: \"Medium\",\n    color: \"oklch(0.627 0.12 164.9)\",\n    icon: AiBrain01Icon,\n  },\n  {\n    id: \"gpt-sol\",\n    label: \"GPT-5.6 Sol\",\n    tier: \"Medium\",\n    color: \"oklch(0.627 0.12 164.9)\",\n    icon: CpuIcon,\n  },\n  {\n    id: \"sonnet\",\n    label: \"Sonnet 5\",\n    tier: \"High\",\n    color: \"oklch(0.673 0.131 41)\",\n    icon: SparklesIcon,\n  },\n  {\n    id: \"opus\",\n    label: \"Opus 4.8\",\n    tier: \"High\",\n    color: \"oklch(0.673 0.131 41)\",\n    icon: AiMagicIcon,\n  },\n  {\n    id: \"fable\",\n    label: \"Fable 5\",\n    tier: \"High\",\n    color: \"oklch(0.673 0.131 41)\",\n    icon: Atom01Icon,\n  },\n];\n\nconst DEFAULT_PHRASES = [\n  \"What's the best way to learn machine learning from scratch?\",\n  \"Help me write a professional email to reschedule a meeting...\",\n  \"Explain the difference between REST and GraphQL APIs.\",\n  \"What are the most effective UI animation principles?\",\n];\n\nconst WAVEFORM_BARS = [\n  { maxH: 5, dur: 0.55 },\n  { maxH: 13, dur: 0.7 },\n  { maxH: 17, dur: 0.5 },\n  { maxH: 9, dur: 0.8 },\n  { maxH: 15, dur: 0.62 },\n  { maxH: 7, dur: 0.68 },\n  { maxH: 12, dur: 0.55 },\n];\n\nfunction resolveList<T>(input: T[] | undefined, fallback: T[]): T[] {\n  return input && input.length > 0 ? input : fallback;\n}\n\nfunction arcAngle(item: ArcItem, i: number, n: number) {\n  if (item.angle !== undefined) return item.angle;\n  if (n <= 1) return 0;\n  const spread = 110;\n  return -spread / 2 + (spread * i) / (n - 1);\n}\n\nfunction arcPosition(angleDeg: number) {\n  const rad = (angleDeg * Math.PI) / 180;\n  return { x: RADIUS * Math.sin(rad), y: -RADIUS * Math.cos(rad) };\n}\n\nfunction WaveformBars() {\n  const reduceMotion = useReducedMotion();\n  return (\n    <div className=\"flex h-5 items-center gap-[2.5px]\">\n      {WAVEFORM_BARS.map(({ maxH, dur }, i) => (\n        <motion.span\n          key={i}\n          className=\"block w-[2.5px] rounded-full bg-rose-500\"\n          animate={\n            reduceMotion ? { height: maxH * 0.6 } : { height: [maxH * 0.25, maxH, maxH * 0.25] }\n          }\n          transition={\n            reduceMotion\n              ? { duration: 0 }\n              : {\n                  duration: dur,\n                  repeat: Infinity,\n                  delay: i * 0.08,\n                  ease: \"easeInOut\",\n                }\n          }\n          style={{ minHeight: 3 }}\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction useTypewriter(text: string, reduceMotion: boolean, speed = 42) {\n  const [displayed, setDisplayed] = useState(\"\");\n\n  useEffect(() => {\n    if (!text) {\n      setDisplayed(\"\");\n      return;\n    }\n    // Under reduced motion, skip the character-by-character reveal.\n    if (reduceMotion) {\n      setDisplayed(text);\n      return;\n    }\n    setDisplayed(\"\");\n    let i = 0;\n    const timer = setInterval(() => {\n      i++;\n      setDisplayed(text.slice(0, i));\n      if (i >= text.length) clearInterval(timer);\n    }, speed);\n    return () => clearInterval(timer);\n  }, [text, reduceMotion, speed]);\n\n  return displayed;\n}\n\nfunction MorphMenu<T>({\n  layoutId,\n  isOpen,\n  onOpen,\n  onClose,\n  reduceMotion,\n  collapsed,\n  triggerLabel,\n  panelWidth,\n  menuLabel,\n  items,\n  getItemKey,\n  isItemActive,\n  onSelect,\n  renderItem,\n  listClassName = \"\",\n  itemClassName,\n}: {\n  layoutId: string;\n  isOpen: boolean;\n  onOpen: () => void;\n  onClose: () => void;\n  reduceMotion: boolean | null;\n  collapsed: React.ReactNode;\n  triggerLabel: string;\n  panelWidth: number;\n  menuLabel: string;\n  items: T[];\n  getItemKey: (item: T) => React.Key;\n  isItemActive: (item: T) => boolean;\n  onSelect: (item: T) => void;\n  renderItem: (item: T, active: boolean) => React.ReactNode;\n  listClassName?: string;\n  itemClassName: (active: boolean) => string;\n}) {\n  const shellTransition = reduceMotion\n    ? { duration: 0 }\n    : isOpen\n      ? EXPAND_SPRING\n      : COLLAPSE_SPRING;\n  const contentIn = reduceMotion ? { duration: 0 } : CONTENT_IN;\n  const contentOut = reduceMotion ? { duration: 0 } : CONTENT_OUT;\n\n  const [activeIndex, setActiveIndex] = React.useState(0);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const menuRef = React.useRef<HTMLDivElement>(null);\n  const itemRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n  // Whether the next close returns focus to the trigger (Escape / select) or\n  // leaves it where the user pointed (outside-pointer dismissal).\n  const restoreFocusRef = React.useRef(true);\n  const wasOpenRef = React.useRef(false);\n\n  // On open: seed the roving index at the current selection and move focus onto\n  // it. On close (only once it has actually been open): return focus to the\n  // trigger, unless an outside pointer opted out.\n  React.useEffect(() => {\n    if (isOpen) {\n      wasOpenRef.current = true;\n      const found = items.findIndex(isItemActive);\n      const idx = found >= 0 ? found : 0;\n      setActiveIndex(idx);\n      const raf = requestAnimationFrame(() => itemRefs.current[idx]?.focus());\n      return () => cancelAnimationFrame(raf);\n    }\n    if (!wasOpenRef.current) return;\n    wasOpenRef.current = false;\n    if (!restoreFocusRef.current) {\n      restoreFocusRef.current = true;\n      return;\n    }\n    const raf = requestAnimationFrame(() => triggerRef.current?.focus());\n    return () => cancelAnimationFrame(raf);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isOpen]);\n\n  // Dismiss on outside pointer without stealing focus back to the trigger.\n  React.useEffect(() => {\n    if (!isOpen) return;\n    const onPointerDown = (e: PointerEvent) => {\n      const t = e.target as Node;\n      if (menuRef.current?.contains(t) || triggerRef.current?.contains(t)) {\n        return;\n      }\n      restoreFocusRef.current = false;\n      onClose();\n    };\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isOpen]);\n\n  const handleSelect = (item: T) => {\n    restoreFocusRef.current = true;\n    onSelect(item);\n  };\n\n  // Roving arrow-key navigation; Escape closes and restores focus to the trigger.\n  const onMenuKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Escape\") {\n      e.preventDefault();\n      restoreFocusRef.current = true;\n      onClose();\n      return;\n    }\n    const count = items.length;\n    if (count === 0) return;\n    let next = activeIndex;\n    switch (e.key) {\n      case \"ArrowDown\":\n        next = (activeIndex + 1) % count;\n        break;\n      case \"ArrowUp\":\n        next = (activeIndex - 1 + count) % count;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = count - 1;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    setActiveIndex(next);\n    itemRefs.current[next]?.focus();\n  };\n\n  return (\n    <div className=\"relative inline-flex flex-shrink-0\">\n      <div\n        aria-hidden\n        className=\"pointer-events-none inline-flex items-center gap-2 px-3.5 py-2 text-xs font-medium whitespace-nowrap opacity-0 select-none\"\n      >\n        {collapsed}\n        <HugeiconsIcon\n          icon={ArrowDown01Icon}\n          size={14}\n          strokeWidth={2}\n          color=\"currentColor\"\n        />\n      </div>\n\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {!isOpen && (\n          <motion.button\n            key=\"collapsed\"\n            ref={triggerRef}\n            layoutId={layoutId}\n            onClick={(e) => {\n              e.stopPropagation();\n              onOpen();\n            }}\n            aria-haspopup=\"menu\"\n            aria-expanded={isOpen}\n            aria-label={triggerLabel}\n            whileTap={{ scale: 0.96 }}\n            transition={shellTransition}\n            style={{ overflow: \"hidden\", borderRadius: 12 }}\n            className=\"absolute inset-y-0 start-0 inline-flex items-center gap-2 bg-muted-foreground px-3.5 py-2 text-xs font-medium text-background select-none\"\n          >\n            <motion.span\n              layout=\"position\"\n              initial={{ opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(4px)\", transition: contentOut }}\n              transition={contentIn}\n              className=\"flex items-center gap-1.5\"\n            >\n              {collapsed}\n              <HugeiconsIcon\n                icon={ArrowDown01Icon}\n                size={12}\n                strokeWidth={2}\n                color=\"currentColor\"\n                aria-hidden={true}\n                className=\"mt-0.5 flex-shrink-0\"\n              />\n            </motion.span>\n          </motion.button>\n        )}\n      </AnimatePresence>\n\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {isOpen && (\n          <motion.div\n            key=\"expanded\"\n            layoutId={layoutId}\n            transition={shellTransition}\n            onClick={(e) => e.stopPropagation()}\n            style={{ width: panelWidth, overflow: \"hidden\", borderRadius: 14 }}\n            className=\"absolute bottom-0 start-0 z-50 bg-muted-foreground p-1.5 text-background ring-1 ring-border\"\n          >\n            <motion.div\n              ref={menuRef}\n              role=\"menu\"\n              aria-label={menuLabel}\n              aria-orientation=\"vertical\"\n              onKeyDown={onMenuKeyDown}\n              layout=\"position\"\n              initial={{ opacity: 0.97, filter: \"blur(4px)\", scale: 0.97 }}\n              animate={{ opacity: 1, filter: \"blur(0px)\", scale: 1 }}\n              exit={{\n                opacity: 0,\n                filter: \"blur(4px)\",\n                scale: 0.97,\n                transition: contentOut,\n              }}\n              transition={contentIn}\n              className={listClassName}\n            >\n              {items.map((item, i) => {\n                const active = isItemActive(item);\n                return (\n                  <button\n                    key={getItemKey(item)}\n                    ref={(el) => {\n                      itemRefs.current[i] = el;\n                    }}\n                    role=\"menuitemradio\"\n                    aria-checked={active}\n                    tabIndex={i === activeIndex ? 0 : -1}\n                    onClick={() => handleSelect(item)}\n                    className={itemClassName(active)}\n                  >\n                    {renderItem(item, active)}\n                  </button>\n                );\n              })}\n            </motion.div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default function AIChat({\n  heading = \"Ready when you are.\",\n  placeholder = \"Our AI is ready to help. Ask it anything...\",\n  arcItems: arcItemsProp,\n  toolItems: toolItemsProp,\n  modes: modesProp,\n  models: modelsProp,\n  mockPhrases: mockPhrasesProp,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onSubmit,\n  defaultModeId,\n  modeId,\n  onModeChange,\n  defaultModelId,\n  modelId,\n  onModelChange,\n  onToolChange,\n  onAttach,\n  onRecordingChange,\n  className = \"\",\n}: AIChatProps = {}) {\n  const arcItems = React.useMemo(\n    () => resolveList(arcItemsProp, DEFAULT_ARC_ITEMS),\n    [arcItemsProp],\n  );\n  const toolItems = React.useMemo(\n    () => resolveList(toolItemsProp, DEFAULT_TOOL_ITEMS),\n    [toolItemsProp],\n  );\n  const modes = React.useMemo(\n    () => resolveList(modesProp, DEFAULT_MODES),\n    [modesProp],\n  );\n  const models = React.useMemo(\n    () => resolveList(modelsProp, DEFAULT_MODELS),\n    [modelsProp],\n  );\n  const mockPhrases = React.useMemo(\n    () => resolveList(mockPhrasesProp, DEFAULT_PHRASES),\n    [mockPhrasesProp],\n  );\n\n  const isValueControlled = value !== undefined;\n  const [valueState, setValueState] = useState(defaultValue);\n  const inputValue = isValueControlled ? value : valueState;\n  const setInputValue = (v: string) => {\n    if (!isValueControlled) setValueState(v);\n    onValueChange?.(v);\n  };\n\n  const isModeControlled = modeId !== undefined;\n  const [modeIdState, setModeIdState] = useState(defaultModeId ?? modes[0]?.id);\n  const currentModeId = isModeControlled ? modeId : modeIdState;\n  const selectedMode = modes.find((m) => m.id === currentModeId) ?? modes[0];\n\n  const isModelControlled = modelId !== undefined;\n  const [modelIdState, setModelIdState] = useState(\n    defaultModelId ?? models[0]?.id,\n  );\n  const currentModelId = isModelControlled ? modelId : modelIdState;\n  const selectedModel =\n    models.find((m) => m.id === currentModelId) ?? models[0];\n\n  const [plusOpen, setPlusOpen] = useState(false);\n  const [menuOpen, setMenuOpen] = useState(false);\n  const [isRecording, setIsRecording] = useState(false);\n  const [currentPhrase, setCurrentPhrase] = useState(\"\");\n  const [selectedTool, setSelectedTool] = useState<ToolItem | null>(null);\n  const [openMenu, setOpenMenu] = useState<\"mode\" | \"model\" | null>(null);\n\n  const reduceMotion = useReducedMotion();\n  const typewriterText = useTypewriter(currentPhrase, !!reduceMotion);\n  const promptId = React.useId();\n  const toolsMenuId = React.useId();\n\n  const closeAll = () => {\n    setPlusOpen(false);\n    setMenuOpen(false);\n    setOpenMenu(null);\n  };\n\n  const openMorph = (which: \"mode\" | \"model\") => {\n    setPlusOpen(false);\n    setMenuOpen(false);\n    setOpenMenu(which);\n  };\n\n  const selectMode = (mode: ChatModeOption) => {\n    if (!isModeControlled) setModeIdState(mode.id);\n    onModeChange?.(mode);\n    setOpenMenu(null);\n  };\n\n  const selectModel = (model: ModelOption) => {\n    if (!isModelControlled) setModelIdState(model.id);\n    onModelChange?.(model);\n    setOpenMenu(null);\n  };\n\n  const selectTool = (tool: ToolItem) => {\n    setSelectedTool((prev) => {\n      const next = prev?.label === tool.label ? null : tool;\n      onToolChange?.(next);\n      return next;\n    });\n    setMenuOpen(false);\n  };\n\n  const setRecording = (next: boolean) => {\n    setIsRecording(next);\n    onRecordingChange?.(next);\n  };\n\n  const toggleRecording = () => {\n    if (!isRecording) {\n      const phrase =\n        mockPhrases[Math.floor(Math.random() * mockPhrases.length)];\n      setCurrentPhrase(phrase);\n      closeAll();\n    } else {\n      setCurrentPhrase(\"\");\n    }\n    setRecording(!isRecording);\n  };\n\n  return (\n    <div\n      className=\"flex h-full w-full flex-col items-center justify-center bg-muted px-4 sm:px-6\"\n      onClick={closeAll}\n    >\n      <motion.h1\n        animate={{ opacity: isRecording ? 0.35 : 1 }}\n        transition={{ duration: 0.3, ease: EASE_OUT_QUART }}\n        className=\"mb-10 text-3xl font-light tracking-tight text-balance text-foreground sm:mb-14 sm:text-4xl\"\n      >\n        {heading}\n      </motion.h1>\n\n      <div\n        className={`relative w-full max-w-2xl ${className}`}\n        onClick={(e) => e.stopPropagation()}\n      >\n        <div\n          id={toolsMenuId}\n          inert={!menuOpen}\n          className=\"pointer-events-none absolute top-full end-4 flex max-w-full flex-wrap items-start justify-end gap-2 pt-1.5\"\n        >\n          {toolItems.map((item, i) => {\n            const active = selectedTool?.label === item.label;\n            return (\n              <motion.button\n                key={item.label}\n                animate={{\n                  y: menuOpen ? 0 : \"-130%\",\n                  opacity: menuOpen ? 1 : 0,\n                  filter: menuOpen ? \"blur(0px)\" : \"blur(2px)\",\n                }}\n                transition={\n                  reduceMotion\n                    ? { duration: 0 }\n                    : {\n                        duration: menuOpen ? 0.4 : 0.35,\n                        delay: menuOpen\n                          ? (toolItems.length - 1 - i) * 0.06\n                          : i * 0.04,\n                        ease: EASE_PANEL,\n                      }\n                }\n                whileTap={{ scale: 0.96 }}\n                onClick={() => selectTool(item)}\n                className={`pointer-events-auto flex items-center gap-2 rounded-[12px] px-3.5 py-2 text-xs font-medium whitespace-nowrap shadow-md transition-colors ${\n                  active\n                    ? \"bg-foreground text-background\"\n                    : \"bg-muted-foreground text-background hover:bg-foreground/90\"\n                }`}\n              >\n                <HugeiconsIcon\n                  icon={item.icon}\n                  size={14}\n                  strokeWidth={1.5}\n                  color=\"currentColor\"\n                />\n                {item.label}\n              </motion.button>\n            );\n          })}\n        </div>\n\n        <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n          {isRecording ? currentPhrase : \"\"}\n        </div>\n\n        <AnimatePresence>\n          {isRecording && (\n            <motion.div\n              initial={{ y: \"-110%\", opacity: 0, scale: 0.95 }}\n              animate={{ y: 0, opacity: 1, scale: 1 }}\n              exit={{ y: \"-110%\", opacity: 0, scale: 0.95 }}\n              transition={{ duration: 0.38, ease: EASE_OUT_QUART }}\n              className=\"absolute top-full inset-x-0 z-0 pt-1.5\"\n            >\n              <div className=\"flex items-start gap-3 rounded-2xl bg-foreground/90 px-5 py-4 text-background backdrop-blur-sm\">\n                <p\n                  aria-hidden={true}\n                  className=\"min-h-[20px] flex-1 text-sm leading-relaxed text-pretty text-background/90\"\n                >\n                  {typewriterText}\n                  <motion.span\n                    animate={reduceMotion ? { opacity: 1 } : { opacity: [1, 0] }}\n                    transition={\n                      reduceMotion\n                        ? { duration: 0 }\n                        : {\n                            duration: 0.5,\n                            repeat: Infinity,\n                            repeatType: \"reverse\",\n                          }\n                    }\n                    className=\"ms-[2px] inline-block h-[14px] w-[2px] rounded-full bg-background/70 align-middle\"\n                  />\n                </p>\n\n                <motion.button\n                  initial={{ opacity: 0, scale: 0.7 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  transition={{\n                    delay: 0.2,\n                    duration: 0.2,\n                    ease: EASE_OUT_QUART,\n                  }}\n                  whileTap={{ scale: 0.96 }}\n                  onClick={() => {\n                    setInputValue(currentPhrase);\n                    setRecording(false);\n                    setCurrentPhrase(\"\");\n                  }}\n                  aria-label=\"Use transcript\"\n                  className=\"relative flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-background/15 transition-colors before:absolute before:-inset-1.5 before:content-[''] hover:bg-background/25\"\n                >\n                  <HugeiconsIcon\n                    icon={ArrowUp01Icon}\n                    size={14}\n                    strokeWidth={2}\n                    color=\"currentColor\"\n                    aria-hidden={true}\n                  />\n                </motion.button>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <div className=\"relative z-10 rounded-[14px] border border-transparent bg-card px-4 pt-3.5 pb-3 shadow-[var(--input-shadow)]\">\n          <label htmlFor={promptId} className=\"sr-only\">\n            {placeholder}\n          </label>\n          <motion.input\n            id={promptId}\n            animate={{ opacity: isRecording ? 0.35 : 1 }}\n            transition={{ duration: 0.3, ease: EASE_OUT_QUART }}\n            type=\"text\"\n            value={inputValue}\n            onChange={(e) => setInputValue(e.target.value)}\n            onFocus={() => setOpenMenu(null)}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" && !e.shiftKey) {\n                e.preventDefault();\n                onSubmit?.(inputValue);\n              }\n            }}\n            placeholder={placeholder}\n            disabled={isRecording}\n            className=\"w-full bg-transparent px-1.5 pb-10 text-base text-foreground transition-colors duration-150 ease-out outline-none placeholder:text-muted-foreground focus:placeholder:text-muted-foreground/60\"\n          />\n\n          <div className=\"flex items-center gap-3 select-none sm:gap-4 pt-10\">\n            <motion.div\n              animate={{ opacity: isRecording ? 0.35 : 1 }}\n              transition={{ duration: 0.3, ease: EASE_OUT_QUART }}\n              className=\"relative flex h-8 w-8 flex-shrink-0 items-center justify-center\"\n            >\n              {plusOpen && (\n                <div\n                  className=\"fixed inset-0 z-10\"\n                  onClick={() => setPlusOpen(false)}\n                />\n              )}\n\n              {arcItems.map((item, i) => {\n                const pos = arcPosition(arcAngle(item, i, arcItems.length));\n                return (\n                  <motion.button\n                    key={item.label}\n                    className=\"absolute z-20 flex flex-col items-center gap-1.5\"\n                    style={{\n                      top: \"50%\",\n                      left: \"50%\",\n                      marginLeft: -22,\n                      marginTop: -22,\n                    }}\n                    animate={{\n                      x: plusOpen ? pos.x : 0,\n                      y: plusOpen ? pos.y : 0,\n                      scale: plusOpen ? 1 : 0,\n                      opacity: plusOpen ? 1 : 0,\n                      filter: plusOpen ? \"blur(0px)\" : \"blur(4px)\",\n                    }}\n                    transition={{\n                      type: \"spring\",\n                      stiffness: 400,\n                      damping: 28,\n                      delay: plusOpen ? i * 0.015 : 0,\n                    }}\n                    whileTap={{ scale: 0.96 }}\n                    aria-label={item.label}\n                    inert={!plusOpen}\n                    onClick={() => {\n                      onAttach?.(item);\n                      setPlusOpen(false);\n                    }}\n                  >\n                    <div className=\"flex h-11 w-11 items-center justify-center rounded-full bg-secondary text-secondary-foreground shadow-[0_1px_2px_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.06)] transition-colors hover:bg-secondary/70\">\n                      <HugeiconsIcon\n                        icon={item.icon}\n                        size={19}\n                        strokeWidth={1.6}\n                        color=\"currentColor\"\n                        aria-hidden={true}\n                      />\n                    </div>\n                  </motion.button>\n                );\n              })}\n\n              <motion.button\n                animate={{ rotate: plusOpen ? 45 : 0 }}\n                transition={{ duration: 0.2, ease: EASE_OUT_QUART }}\n                whileTap={{ scale: 0.96 }}\n                onClick={(e) => {\n                  e.stopPropagation();\n                  setOpenMenu(null);\n                  setMenuOpen(false);\n                  setPlusOpen((v) => !v);\n                }}\n                aria-label=\"Add attachment\"\n                aria-expanded={plusOpen}\n                className=\"relative z-20 flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors before:absolute before:-inset-1 before:content-[''] hover:bg-accent hover:text-foreground\"\n              >\n                <HugeiconsIcon\n                  icon={Add01Icon}\n                  size={20}\n                  strokeWidth={1.8}\n                  aria-hidden={true}\n                />\n              </motion.button>\n            </motion.div>\n\n            <div className=\"flex items-center gap-2\">\n              <motion.div\n                animate={{ opacity: isRecording ? 0.35 : 1 }}\n                transition={{ duration: 0.22, ease: EASE_OUT_QUART }}\n              >\n                <MorphMenu\n                  layoutId=\"chat-mode-shell\"\n                  isOpen={openMenu === \"mode\"}\n                  onOpen={() => !isRecording && openMorph(\"mode\")}\n                  onClose={() => setOpenMenu(null)}\n                  reduceMotion={reduceMotion}\n                  triggerLabel={`Mode: ${selectedMode.label}`}\n                  panelWidth={230}\n                  menuLabel=\"Select a mode\"\n                  items={modes}\n                  getItemKey={(mode) => mode.id}\n                  isItemActive={(mode) => mode.id === selectedMode.id}\n                  onSelect={selectMode}\n                  listClassName=\"flex flex-col gap-1\"\n                  itemClassName={(active) =>\n                    `flex flex-col gap-1 rounded-[9px] px-3 py-2 text-start transition-[background-color,transform] select-none active:scale-[0.96] ${\n                      active ? \"bg-background/10\" : \"hover:bg-background/5\"\n                    }`\n                  }\n                  collapsed={\n                    <span className=\"flex items-center gap-2\">\n                      <HugeiconsIcon\n                        icon={selectedMode.icon}\n                        size={14}\n                        strokeWidth={1.5}\n                        color=\"currentColor\"\n                        aria-hidden={true}\n                      />\n                      <span className=\"max-[440px]:hidden\">\n                        {selectedMode.label}\n                      </span>\n                    </span>\n                  }\n                  renderItem={(mode, active) => (\n                    <>\n                      <span className=\"flex items-center gap-1 text-sm font-medium text-background tracking-normal\">\n                        <span className=\"flex-shrink-0 text-background\">\n                          <HugeiconsIcon\n                            icon={mode.icon}\n                            size={14}\n                            strokeWidth={1.8}\n                            color=\"currentColor\"\n                            aria-hidden={true}\n                          />\n                        </span>\n                        {mode.label}\n                        {active && (\n                          <HugeiconsIcon\n                            icon={Tick02Icon}\n                            size={15}\n                            strokeWidth={2.2}\n                            color=\"currentColor\"\n                            aria-hidden={true}\n                            className=\"ms-auto\"\n                          />\n                        )}\n                      </span>\n                      <span className=\"text-xs text-pretty text-background/90 tracking-tight\">\n                        {mode.desc}\n                      </span>\n                    </>\n                  )}\n                />\n              </motion.div>\n\n              <motion.div\n                animate={{ opacity: isRecording ? 0.35 : 1 }}\n                transition={{ duration: 0.3, ease: EASE_OUT_QUART }}\n              >\n                <MorphMenu\n                  layoutId=\"chat-model-shell\"\n                  isOpen={openMenu === \"model\"}\n                  onOpen={() => !isRecording && openMorph(\"model\")}\n                  onClose={() => setOpenMenu(null)}\n                  reduceMotion={reduceMotion}\n                  triggerLabel={`Model: ${selectedModel.label}`}\n                  panelWidth={280}\n                  menuLabel=\"Select a model\"\n                  items={models}\n                  getItemKey={(model) => model.id}\n                  isItemActive={(model) => model.id === selectedModel.id}\n                  onSelect={selectModel}\n                  listClassName=\"max-h-60 overflow-y-auto\"\n                  itemClassName={() =>\n                    \"flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-start transition-[background-color,transform] select-none hover:bg-background/5 active:scale-[0.96]\"\n                  }\n                  collapsed={\n                    <span className=\"inline-flex items-center gap-1.5 align-middle\">\n                      {selectedModel.icon && (\n                        <HugeiconsIcon\n                          icon={selectedModel.icon}\n                          size={14}\n                          strokeWidth={1.5}\n                          color=\"currentColor\"\n                          aria-hidden={true}\n                          className=\"flex-shrink-0\"\n                        />\n                      )}\n                      <span className=\"inline-block max-w-[8rem] truncate max-[440px]:max-w-[4.5rem]\">\n                        {selectedModel.label}\n                      </span>\n                    </span>\n                  }\n                  renderItem={(model, active) => (\n                    <>\n                      <span\n                        className=\"flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold text-white\"\n                        style={{ background: model.color }}\n                        aria-hidden={true}\n                      >\n                        {model.icon ? (\n                          <HugeiconsIcon\n                            icon={model.icon}\n                            size={14}\n                            strokeWidth={2}\n                            color=\"currentColor\"\n                          />\n                        ) : (\n                          model.label[0]\n                        )}\n                      </span>\n                      <span className=\"flex-1 text-sm text-background\">\n                        {model.label}{\" \"}\n                        <span className=\"text-background/60\">{model.tier}</span>\n                      </span>\n                      {active && (\n                        <HugeiconsIcon\n                          icon={Tick02Icon}\n                          size={16}\n                          strokeWidth={2.2}\n                          color=\"currentColor\"\n                          aria-hidden={true}\n                        />\n                      )}\n                    </>\n                  )}\n                />\n              </motion.div>\n            </div>\n\n            <div className=\"ms-auto flex flex-shrink-0 items-center gap-2\">\n              <motion.button\n                animate={{\n                  rotate: menuOpen ? 20 : 0,\n                  scale: menuOpen ? 1.15 : 1,\n                  opacity: isRecording ? 0.35 : 1,\n                }}\n                whileTap={{ scale: 0.96 }}\n                transition={{ duration: 0.2, ease: EASE_OUT_QUART }}\n                onClick={(e) => {\n                  e.stopPropagation();\n                  if (!isRecording) {\n                    setOpenMenu(null);\n                    setPlusOpen(false);\n                    setMenuOpen((v) => !v);\n                  }\n                }}\n                className={`relative flex h-8 w-8 items-center justify-center rounded-full transition-colors before:absolute before:-inset-1 before:content-[''] ${\n                  selectedTool\n                    ? \"bg-foreground text-background hover:bg-foreground/90\"\n                    : \"text-foreground hover:bg-accent hover:text-foreground\"\n                }`}\n                aria-label={selectedTool ? selectedTool.label : \"Tools\"}\n                aria-expanded={menuOpen}\n                aria-controls={toolsMenuId}\n                title={selectedTool ? selectedTool.label : \"Tools\"}\n              >\n                <AnimatePresence mode=\"popLayout\" initial={false}>\n                  <motion.span\n                    key={selectedTool?.label ?? \"sparkles\"}\n                    initial={{ opacity: 0, scale: 0.25, filter: \"blur(2px)\" }}\n                    animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                    exit={{ opacity: 0, scale: 0.25, filter: \"blur(2px)\" }}\n                    transition={\n                      reduceMotion\n                        ? { duration: 0 }\n                        : { duration: 0.2, ease: \"easeInOut\" }\n                    }\n                    className=\"flex items-center justify-center\"\n                  >\n                    <HugeiconsIcon\n                      icon={selectedTool ? selectedTool.icon : SparklesIcon}\n                      size={20}\n                      strokeWidth={1.8}\n                      color=\"currentColor\"\n                      aria-hidden={true}\n                    />\n                  </motion.span>\n                </AnimatePresence>\n              </motion.button>\n\n              <motion.button\n                onClick={(e) => {\n                  e.stopPropagation();\n                  toggleRecording();\n                }}\n                aria-label={\n                  isRecording ? \"Stop voice input\" : \"Start voice input\"\n                }\n                aria-pressed={isRecording}\n                className=\"relative flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors before:absolute before:-inset-1 before:content-[''] hover:bg-accent hover:text-foreground\"\n                animate={{ scale: isRecording ? 1.05 : 1 }}\n                whileTap={{ scale: 0.96 }}\n                transition={{ duration: 0.2, ease: EASE_OUT_QUART }}\n              >\n                <AnimatePresence mode=\"popLayout\" initial={false}>\n                  {isRecording ? (\n                    <motion.div\n                      key=\"waveform\"\n                      aria-hidden={true}\n                      initial={{ opacity: 0, scale: 0.25, filter: \"blur(2px)\" }}\n                      animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                      exit={{ opacity: 0, scale: 0.25, filter: \"blur(2px)\" }}\n                      transition={\n                        reduceMotion\n                          ? { duration: 0 }\n                          : { duration: 0.2, ease: \"easeInOut\" }\n                      }\n                    >\n                      <WaveformBars />\n                    </motion.div>\n                  ) : (\n                    <motion.div\n                      key=\"mic\"\n                      initial={{ opacity: 0, scale: 0.25, filter: \"blur(2px)\" }}\n                      animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                      exit={{ opacity: 0, scale: 0.25, filter: \"blur(2px)\" }}\n                      transition={\n                        reduceMotion\n                          ? { duration: 0 }\n                          : { duration: 0.2, ease: \"easeInOut\" }\n                      }\n                    >\n                      <HugeiconsIcon\n                        icon={Mic01Icon}\n                        size={20}\n                        strokeWidth={1.8}\n                        color=\"currentColor\"\n                        aria-hidden={true}\n                      />\n                    </motion.div>\n                  )}\n                </AnimatePresence>\n              </motion.button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "~/components/craftui/blocks/ai-chat/default.tsx"
    }
  ],
  "meta": {
    "displayName": "AI Chat - AI Chat",
    "description": "A theme-token-driven AI prompt composer. Mode and model pickers morph between a collapsed pill and an expanded panel through a shared layoutId spring with a blur cross-fade; the plus button fans out a radial attachment menu; tool chips reveal with a staggered cross-blur; the mic cross-fades to an animated waveform while a typewriter transcript slides in. Attachments, tools, modes, models, and transcript phrases are all props, with controlled/uncontrolled selection per dimension. The pickers ship as full APG menus (roving tabindex, arrow-key navigation, focus restore), every control carries an accessible name, closed reveals are inert, the transcript announces through an aria-live region, and all motion honors prefers-reduced-motion.",
    "category": "Chat UI",
    "tags": [
      "chat",
      "ai",
      "prompt",
      "input",
      "composer",
      "morph",
      "layoutId",
      "radial-menu",
      "voice",
      "waveform",
      "dropdown",
      "spring",
      "interactive"
    ],
    "animationType": "click"
  }
}