{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ask-ai",
  "type": "registry:ui",
  "title": "Ask AI",
  "description": "A popover prompt launcher with an animated mascot trigger, one-click query links for major AI providers, and instant clipboard fallback.",
  "author": "Aditya Ojha <https://akoder.xyz>",
  "categories": [
    "ai",
    "assistant",
    "popover",
    "dialog",
    "interactive"
  ],
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "popover",
    "tooltip"
  ],
  "meta": {
    "docs": "https://akoder.xyz/components/ask-ai",
    "source": "https://github.com/AdityaKodez/adityaojha/blob/main/components/ui/ask-ai.tsx"
  },
  "files": [
    {
      "path": "components/ui/ask-ai.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\";\nimport { Check, Copy, ArrowUpRight } from \"lucide-react\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n  PopoverTitle,\n  PopoverDescription,\n} from \"@/components/ui/popover\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport type MascotGaze = \"up\" | \"down\" | \"left\" | \"right\";\n\nexport function AIMascot({\n  awake = false,\n  gaze,\n  size = \"default\",\n  brand = false,\n  className,\n}: {\n  awake?: boolean;\n  /** Direction the eyes look while awake: point it at the popover. */\n  gaze?: MascotGaze;\n  size?: \"default\" | \"compact\";\n  brand?: boolean;\n  className?: string;\n}) {\n  const eyeShift = !awake\n    ? \"translate-x-px -translate-y-px\"\n    : gaze === \"down\"\n      ? \"translate-x-px translate-y-1\"\n      : gaze === \"left\"\n        ? \"-translate-x-1 -translate-y-px\"\n        : gaze === \"right\"\n          ? \"translate-x-1 -translate-y-px\"\n          : \"translate-x-px -translate-y-1\";\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-awake={awake}\n      className={cn(\n        \"relative inline-flex shrink-0 items-center justify-center bg-primary text-primary-foreground animate-[ai-mascot-blob_9s_ease-in-out_infinite] transition-transform duration-[440ms] ease-[cubic-bezier(.22,1.5,.5,1)]\",\n        brand\n          ? \"h-[22px] w-[22px] shadow-none\"\n          : size === \"compact\"\n            ? \"h-7 w-7 shadow-none\"\n            : \"h-10 w-10 rotate-[-7deg] data-[awake=true]:rotate-6 data-[awake=true]:scale-[1.05]\",\n        className\n      )}\n    >\n      <span\n        className={cn(\n          \"flex transition-transform duration-300\",\n          brand\n            ? \"gap-1\"\n            : size === \"compact\"\n              ? \"gap-[5px]\"\n              : cn(\"gap-2 transition-transform\", eyeShift)\n        )}\n      >\n        <span\n          className={cn(\n            \"block animate-[ai-mascot-blink_6.5s_infinite] rounded-[5px] bg-current\",\n            brand ? \"h-[5px] w-[2.5px]\" : size === \"compact\" ? \"h-[7px] w-[3px]\" : \"h-2.5 w-1\"\n          )}\n        />\n        <span\n          className={cn(\n            \"block animate-[ai-mascot-blink_6.5s_infinite] rounded-[5px] bg-current\",\n            brand ? \"h-[5px] w-[2.5px]\" : size === \"compact\" ? \"h-[7px] w-[3px]\" : \"h-2.5 w-1\"\n          )}\n        />\n      </span>\n      <style>{`\n        @keyframes ai-mascot-blob {\n          0%, 100% { border-radius: 58% 42% 55% 45% / 52% 58% 42% 48%; }\n          33% { border-radius: 45% 55% 48% 52% / 58% 44% 56% 42%; }\n          66% { border-radius: 52% 48% 42% 58% / 45% 52% 48% 55%; }\n        }\n        @keyframes ai-mascot-blink {\n          0%, 42%, 46%, 100% { transform: scaleY(1); }\n          44% { transform: scaleY(.12); }\n        }\n      `}</style>\n    </span>\n  );\n}\n\nexport type IconProps = React.SVGProps<SVGSVGElement>;\n\nexport function OpenAIIcon(props: IconProps) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 260\" fill=\"currentColor\" {...props}>\n      <path d=\"M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z\" />\n    </svg>\n  );\n}\n\nexport function ClaudeIcon(props: IconProps) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 257\" fill=\"currentColor\" {...props}>\n      <path d=\"m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z\" />\n    </svg>\n  );\n}\n\nexport function GrokIcon(props: IconProps) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1024 1024\" fill=\"currentColor\" {...props}>\n      <path d=\"M395.479 633.828 735.91 381.105c16.689-12.39 40.544-7.557 48.496 11.687 41.854 101.493 23.155 223.461-60.118 307.204-83.272 83.743-199.137 102.108-305.041 60.281l-115.691 53.866c165.934 114.059 367.431 85.852 493.345-40.861 99.875-100.439 130.807-237.345 101.884-360.806l.262.263c-41.942-181.369 10.311-253.865 117.353-402.107 2.53-3.515 5.07-7.03 7.6-10.632L883.144 141.651v-.439L395.392 633.916\" />\n      <path d=\"M325.226 695.251C206.128 580.84 226.662 403.776 328.285 301.668c75.146-75.571 198.264-106.414 305.741-61.072l115.428-53.602c-20.797-15.114-47.448-31.371-78.03-42.794-138.234-57.206-303.731-28.735-416.101 84.182C147.234 337.081 113.244 504.215 171.613 646.833c43.603 106.59-27.874 181.985-99.875 258.083C46.224 931.893 20.622 958.87 0 987.429l325.139-292.09\" />\n    </svg>\n  );\n}\n\nexport function PerplexityIcon(props: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 48 48\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      {...props}\n    >\n      <path d=\"M24 4.5v39M13.73 16.573v-9.99L24 16.573m0 14.5L13.73 41.417V27.01L24 16.573m0 0l10.27-9.99v9.99\" />\n      <path d=\"M13.73 31.396H9.44V16.573h29.12v14.823h-4.29\" />\n      <path d=\"M24 16.573L34.27 27.01v14.407L24 31.073\" />\n    </svg>\n  );\n}\n\nexport const aiProviderIcons = {\n  chatgpt: OpenAIIcon,\n  claude: ClaudeIcon,\n  grok: GrokIcon,\n  perplexity: PerplexityIcon,\n} as const;\n\nexport type AIProvider = {\n  id: string;\n  name: string;\n  url: string;\n  promptParam?: string;\n  brandColor: string;\n};\n\nexport const defaultAIProviders: readonly AIProvider[] = [\n  {\n    id: \"chatgpt\",\n    name: \"ChatGPT\",\n    url: \"https://chatgpt.com/\",\n    promptParam: \"q\",\n    brandColor: \"var(--foreground)\",\n  },\n  {\n    id: \"claude\",\n    name: \"Claude\",\n    url: \"https://claude.ai/new\",\n    promptParam: \"q\",\n    brandColor: \"#D97757\",\n  },\n  {\n    id: \"grok\",\n    name: \"Grok\",\n    url: \"https://grok.com/\",\n    promptParam: \"q\",\n    brandColor: \"var(--foreground)\",\n  },\n  {\n    id: \"perplexity\",\n    name: \"Perplexity\",\n    url: \"https://www.perplexity.ai/search\",\n    promptParam: \"q\",\n    brandColor: \"#20B8CD\",\n  },\n];\n\nexport function getProviderUrl(provider: AIProvider, prompt: string) {\n  const url = new URL(provider.url);\n  if (url.protocol !== \"https:\") {\n    throw new Error(\"AI provider URLs must use HTTPS.\");\n  }\n  if (provider.promptParam) {\n    url.searchParams.set(provider.promptParam, prompt);\n  }\n  return url.toString();\n}\n\nexport type AskAIProps = {\n  prompt: string;\n  title?: string;\n  description?: string;\n  label?: string;\n  /** Optional tooltip text shown on hover. When blobOnly is true, defaults to label or \"ask an ai\". */\n  tooltip?: string;\n  /** Render only the mascot blob in a circular trigger without the label text. */\n  blobOnly?: boolean;\n  /** Optional custom mascot node, rendered in place of the default blob. */\n  mascot?: ReactNode;\n  providers?: readonly AIProvider[];\n  size?: \"default\" | \"compact\";\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  align?: \"start\" | \"center\" | \"end\";\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n  style?: CSSProperties;\n};\n\nexport function AskAI({\n  prompt,\n  title = \"Ask an AI about me\",\n  description = \"A fresh perspective, from your favorite assistant.\",\n  label = \"Ask an AI\",\n  tooltip,\n  blobOnly = false,\n  mascot,\n  providers = defaultAIProviders,\n  size = \"default\",\n  side = \"top\",\n  align = \"start\",\n  defaultOpen = false,\n  open,\n  onOpenChange,\n  className,\n  style,\n}: AskAIProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [tooltipOpen, setTooltipOpen] = useState(false);\n  const [copied, setCopied] = useState(false);\n  const [fallbackPrompt, setFallbackPrompt] = useState(false);\n  const resetTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const isOpen = open ?? internalOpen;\n\n  useEffect(() => () => {\n    if (resetTimeout.current) clearTimeout(resetTimeout.current);\n  }, []);\n\n  function changeOpen(next: boolean) {\n    setInternalOpen(next);\n    onOpenChange?.(next);\n    if (next) {\n      setTooltipOpen(false);\n    }\n    if (!next) {\n      setFallbackPrompt(false);\n    }\n  }\n\n  async function copyPrompt() {\n    try {\n      await navigator.clipboard.writeText(prompt);\n      setCopied(true);\n      setFallbackPrompt(false);\n      if (resetTimeout.current) clearTimeout(resetTimeout.current);\n      resetTimeout.current = setTimeout(() => setCopied(false), 2500);\n    } catch {\n      setFallbackPrompt(true);\n    }\n  }\n\n  const tooltipText = tooltip ?? (blobOnly ? label : undefined);\n\n  const triggerButton = (\n    <PopoverTrigger\n      className={cn(\n        \"group inline-flex cursor-pointer items-center justify-center text-foreground transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        blobOnly\n          ? cn(\n              \"rounded-full bg-background/55 backdrop-blur-xl backdrop-saturate-150 shadow-sm hover:shadow-md\",\n              size === \"compact\" ? \"size-10\" : \"size-12\"\n            )\n          : cn(\n              \"h-14 gap-4 rounded-full bg-muted/30 ps-5 pe-3 text-base font-medium tracking-tight data-[size=compact]:h-11 data-[size=compact]:gap-3 data-[size=compact]:rounded-[13px] data-[size=compact]:ps-3.5 data-[size=compact]:pe-[9px] data-[size=compact]:text-sm\"\n            ),\n        className\n      )}\n      data-size={size}\n      style={style}\n      aria-label={blobOnly ? tooltipText || label : undefined}\n    >\n      {!blobOnly && <span>{label}</span>}\n      <span className=\"transition-transform duration-200 group-hover:-translate-y-0.5\">\n        {mascot ?? (\n          <AIMascot\n            awake={isOpen}\n            gaze={side === \"top\" ? \"up\" : side === \"bottom\" ? \"down\" : side}\n            size={size}\n          />\n        )}\n      </span>\n    </PopoverTrigger>\n  );\n\n  return (\n    <TooltipProvider delayDuration={250}>\n      <Popover open={isOpen} onOpenChange={changeOpen}>\n        {tooltipText ? (\n          <Tooltip\n            open={isOpen ? false : tooltipOpen}\n            onOpenChange={(next) => setTooltipOpen(isOpen ? false : next)}\n          >\n            <TooltipTrigger asChild>\n              {triggerButton}\n            </TooltipTrigger>\n            <TooltipContent side={side === \"top\" ? \"left\" : \"top\"} className=\"text-xs\">\n              {tooltipText}\n            </TooltipContent>\n          </Tooltip>\n        ) : (\n          triggerButton\n        )}\n        <PopoverContent\n          side={side}\n          align={align}\n          sideOffset={16}\n          autoFocus={false}\n          className=\"w-82.5 sm:w-96 max-w-[calc(100vw-24px)] gap-0 rounded-4xl sm:rounded-[25px] bg-background/80 backdrop-blur-xl backdrop-saturate-150 p-4 sm:px-5 sm:pt-5 sm:pb-4 text-foreground shadow-xl ring-1 ring-inset ring-border/50\"\n        >\n          <PopoverTitle className=\"text-sm sm:text-base font-medium tracking-tight\">\n            {title}\n          </PopoverTitle>\n          <PopoverDescription className=\"mt-1 sm:mt-1.5 text-xs sm:text-sm leading-relaxed text-muted-foreground\">\n            {description}\n          </PopoverDescription>\n          <div className=\"mt-3.5 sm:mt-[23px] flex gap-1 sm:gap-1.5\" aria-label=\"Choose your AI assistant\">\n            {providers.map((provider) => {\n              const Icon = aiProviderIcons[provider.id as keyof typeof aiProviderIcons];\n              return (\n                <Tooltip key={provider.id}>\n                  <TooltipTrigger asChild>\n                    <a\n                      href={getProviderUrl(provider, prompt)}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      style={{ \"--brand\": provider.brandColor } as CSSProperties}\n                      className=\"group bg-muted hover:bg-background relative flex h-16 sm:h-[77px] min-w-0 flex-1 items-center justify-center rounded-[12px] sm:rounded-[15px] bg-card text-foreground/50 duration-200 ease-out hover:-translate-y-1 hover:text-[var(--brand)] focus-visible:-translate-y-1 focus-visible:bg-primary/[0.08] focus-visible:text-[var(--brand)] focus-visible:shadow-[0_6px_10px_-5px_color-mix(in_srgb,var(--primary)_18%,transparent)] focus-visible:outline-none first:rounded-[16px_12px_12px_16px] sm:first:rounded-[21px_15px_15px_21px] last:rounded-[12px_16px_16px_12px] sm:last:rounded-[15px_21px_21px_15px]\"\n                      aria-label={`Ask ${provider.name} (opens in a new tab)${!provider.promptParam ? \"; copies prompt to paste\" : \"\"}`}\n                      onClick={() => {\n                        if (!provider.promptParam) void copyPrompt();\n                      }}\n                    >\n                      {Icon ? (\n                        <Icon\n                          aria-hidden=\"true\"\n                          className=\"size-[22px] sm:size-[27px] transition-transform duration-200 group-hover:-translate-y-1 sm:group-hover:-translate-y-[7px] group-hover:scale-[.92] group-focus-visible:-translate-y-1 sm:group-focus-visible:-translate-y-[7px] group-focus-visible:scale-[.92]\"\n                        />\n                      ) : null}\n                      <span className=\"absolute bottom-1.5 sm:bottom-2 translate-y-1 text-[9px] sm:text-[10px] opacity-0 transition-[opacity,transform] duration-[180ms] group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100\">\n                        {provider.name}\n                      </span>\n                      <ArrowUpRight\n                        aria-hidden=\"true\"\n                        className=\"absolute right-1 sm:right-[7px] top-1 sm:top-[7px] size-2.5 sm:size-[11px] text-primary opacity-0 transition-opacity duration-[180ms] group-hover:opacity-100 group-focus-visible:opacity-100\"\n                      />\n                    </a>\n                  </TooltipTrigger>\n                  <TooltipContent className=\"text-xs\">\n                    {provider.promptParam ? `open ${provider.name}` : `copy prompt & open ${provider.name}`}\n                  </TooltipContent>\n                </Tooltip>\n              );\n            })}\n          </div>\n          <button\n            type=\"button\"\n            className=\"mt-2.5 sm:mt-3 flex min-h-9 sm:min-h-10 w-full cursor-pointer items-center justify-center gap-1.5 sm:gap-2 rounded-xl sm:rounded-2xl text-[11px] sm:text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground [&_svg]:size-3 sm:[&_svg]:size-3.5\"\n            onClick={() => void copyPrompt()}\n          >\n            {copied ? <Check aria-hidden=\"true\" /> : <Copy aria-hidden=\"true\" />}\n            <span>{copied ? \"copied!\" : \"copy the prompt instead\"}</span>\n          </button>\n\n          {fallbackPrompt && (\n            <textarea\n              className=\"mt-2.5 sm:mt-3 w-full rounded-lg border border-border bg-background p-2 text-xs sm:text-sm text-foreground outline-none focus:ring-2 focus:ring-ring\"\n              aria-label=\"Select and copy this prompt\"\n              readOnly\n              value={prompt}\n              onFocus={(event) => event.currentTarget.select()}\n              rows={3}\n            />\n          )}\n        </PopoverContent>\n      </Popover>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "## Installation\n\n```bash\nnpx shadcn@latest add @akoder/ask-ai\n```\n\n## Usage\n\n```tsx\nimport { AskAI } from \"@/components/ui/ask-ai\";\n\nexport function Hero() {\n  return (\n    <AskAI\n      prompt=\"Introduce this project and explain what makes it unique.\"\n      label=\"ask an ai\"\n    />\n  );\n}\n```"
}