shadimage
8starlabs-ui
uiopen-in-chatsource: ok · hit

Open In Chat

A component that opens an LLM chat with a pre-filled prompt.

npx shadcn add "https://ui.8starlabs.com/r/open-in-chat.json"
free

Where it goes

chat

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Registry dependencies

https://ui.8starlabs.com/r/button.jsonhttps://ui.8starlabs.com/r/dropdown-menu.jsonhttps://ui.8starlabs.com/r/tooltip.json

Files2 · source cached 10/14/2026, 3:00:54 PM

registry/8starlabs-ui/blocks/open-in-chat.tsx
"use client";

import * as React from "react";
import { ChevronDown, ExternalLink } from "lucide-react";

import {
  ClaudeIcon,
  GrokIcon,
  OpenAIIcon,
  PerplexityIcon,
  V0Icon
} from "@/registry/8starlabs-ui/ui/ai-icons";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/8starlabs-ui/blocks/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger
} from "@/registry/8starlabs-ui/ui/dropdown-menu";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger
} from "@/registry/8starlabs-ui/ui/tooltip";

export type AiLinkName = "ChatGPT" | "Claude" | "Perplexity" | "Grok" | "v0";
export type OpenInChatLayout = "dropdown" | "horizontal";

export type OpenInChatIcon = React.ElementType<React.ComponentProps<"svg">>;

export type AiLinkData = {
  baseUrl: string;
  paramName?: string;
  icon: OpenInChatIcon;
};

export type OpenInChatContextValue = {
  prompt: string;
  layout: OpenInChatLayout;
};

export type OpenInChatBaseProps = Omit<
  React.ComponentPropsWithoutRef<"div">,
  "align"
> & {
  prompt: string;
  triggerLabel?: React.ReactNode;
  triggerClassName?: string;
  contentClassName?: string;
  align?: React.ComponentProps<typeof DropdownMenuContent>["align"];
  side?: React.ComponentProps<typeof DropdownMenuContent>["side"];
};

export type OpenInChatDropdownProps = OpenInChatBaseProps & {
  layout?: "dropdown";
  trigger?: React.ReactElement | null;
};

export type OpenInChatHorizontalProps = OpenInChatBaseProps & {
  layout: "horizontal";
  trigger?: never;
};

export type OpenInChatProps =
  | OpenInChatDropdownProps
  | OpenInChatHorizontalProps;

export type OpenInChatLinkProps = {
  baseUrl: string;
  paramName?: string;
  icon: OpenInChatIcon;
  label: string;
  prompt?: string;
  className?: string;
};

const OpenInChatContext = React.createContext<OpenInChatContextValue | null>(
  null
);

export const aiLinksDataMap: Record<AiLinkName, AiLinkData> = {
  ChatGPT: {
    baseUrl: "https://chatgpt.com/",
    icon: OpenAIIcon
  },
  Claude: {
    baseUrl: "https://claude.ai/new",
    icon: ClaudeIcon
  },
  Perplexity: {
    baseUrl: "https://www.perplexity.ai/search/new",
    icon: PerplexityIcon
  },
  Grok: {
    baseUrl: "https://grok.com/",
    icon: GrokIcon
  },
  v0: {
    baseUrl: "https://v0.app/chat",
    icon: V0Icon
  }
};

export function getPromptUrl(
  baseUrl: string,
  prompt: string,
  paramName: string = "q"
) {
  const url = new URL(baseUrl);
  url.searchParams.set(paramName, prompt);
  return url.toString();
}

export function useOpenInChat() {
  const context = React.useContext(OpenInChatContext);

  if (!context) {
    throw new Error("OpenInChat links must be used inside OpenInChat.");
  }

  return context;
}

export default function OpenInChat({
  prompt,
  children,
  layout = "dropdown",
  trigger,
  triggerLabel = "Open in chat",
  className,
  triggerClassName,
  contentClassName,
  align = "end",
  side,
  ...props
}: OpenInChatProps) {
  const contextValue = React.useMemo(
    () => ({ prompt, layout }),
    [prompt, layout]
  );
  const items =
    React.Children.count(children) > 0
      ? children
      : (Object.entries(aiLinksDataMap) as [AiLinkName, AiLinkData][]).map(
          ([label, dat], idx) => (
            <OpenInChatLink
              label={label}
              baseUrl={dat.baseUrl}
              icon={dat.icon}
              paramName={dat.paramName}
              key={idx}
            />
          )
        );

  if (layout === "horizontal") {
    return (
      <TooltipProvider>
        <OpenInChatContext.Provider value={contextValue}>
          <div
            {...props}
            className={cn(
              "border-border bg-card text-muted-foreground inline-flex w-fit items-center justify-center gap-1.5 rounded-md border px-2 py-1 text-sm shadow-xs",
              triggerClassName,
              className
            )}
          >
            {triggerLabel ? <span>{triggerLabel}</span> : null}
            {items}
          </div>
        </OpenInChatContext.Provider>
      </TooltipProvider>
    );
  }

  return (
    <OpenInChatContext.Provider value={contextValue}>
      <div {...props} className={cn("inline-flex", className)}>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              trigger ?? (
                <Button
                  type="button"
                  variant="outline"
                  className={cn("gap-2", triggerClassName)}
                >
                  {triggerLabel}
                  <ChevronDown
                    className="size-4 opacity-70"
                    aria-hidden="true"
                  />
                </Button>
              )
            }
          />
          <DropdownMenuContent
            align={align}
            side={side}
            className={cn("w-56", contentClassName)}
          >
            {items}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
    </OpenInChatContext.Provider>
  );
}

export function OpenInChatLink({
  label,
  prompt,
  className,
  baseUrl,
  paramName = "q",
  icon
}: OpenInChatLinkProps) {
  const { prompt: contextPrompt, layout } = useOpenInChat();

  const href = getPromptUrl(baseUrl, prompt ?? contextPrompt, paramName);

  const Icon = icon;

  if (layout === "horizontal") {
    return (
      <Tooltip>
        <TooltipTrigger
          render={
            <a
              href={href}
              target="_blank"
              rel="noreferrer"
              aria-label={`Open in ${label}`}
              className={cn(
                "text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-6 shrink-0 items-center justify-center rounded-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
                className
              )}
            >
              <Icon className="size-4" aria-hidden="true" />
            </a>
          }
        />
        <TooltipContent sideOffset={6}>Open in {label}</TooltipContent>
      </Tooltip>
    );
  }

  return (
    <DropdownMenuItem
      className={cn("cursor-pointer", className)}
      render={
        <a href={href} target="_blank" rel="noreferrer">
          <Icon className="size-4" aria-hidden="true" />
          <span className="flex-1">Open in {label}</span>
          <ExternalLink className="size-4 opacity-60" aria-hidden="true" />
        </a>
      }
    />
  );
}

export type OpenInChatProviderLinkProps = Pick<
  OpenInChatLinkProps,
  "prompt" | "className"
>;

export function OpenInChatGPT(params: OpenInChatProviderLinkProps) {
  const dat = aiLinksDataMap.ChatGPT;

  return (
    <OpenInChatLink
      label="ChatGPT"
      baseUrl={dat.baseUrl}
      icon={dat.icon}
      paramName={dat.paramName}
      {...params}
    />
  );
}

export function OpenInClaude(params: OpenInChatProviderLinkProps) {
  const dat = aiLinksDataMap.Claude;

  return (
    <OpenInChatLink
      label="Claude"
      baseUrl={dat.baseUrl}
      icon={dat.icon}
      paramName={dat.paramName}
      {...params}
    />
  );
}

export function OpenInPerplexity(params: OpenInChatProviderLinkProps) {
  const dat = aiLinksDataMap.Perplexity;

  return (
    <OpenInChatLink
      label="Perplexity"
      baseUrl={dat.baseUrl}
      icon={dat.icon}
      paramName={dat.paramName}
      {...params}
    />
  );
}

export function OpenInGrok(params: OpenInChatProviderLinkProps) {
  const dat = aiLinksDataMap.Grok;

  return (
    <OpenInChatLink
      label="Grok"
      baseUrl={dat.baseUrl}
      icon={dat.icon}
      paramName={dat.paramName}
      {...params}
    />
  );
}

export function OpenInV0(params: OpenInChatProviderLinkProps) {
  const dat = aiLinksDataMap.v0;

  return (
    <OpenInChatLink
      label="v0"
      baseUrl={dat.baseUrl}
      icon={dat.icon}
      paramName={dat.paramName}
      {...params}
    />
  );
}
registry/8starlabs-ui/ui/ai-icons.tsx
import { forwardRef } from "react";
import { siClaude, siPerplexity, siV0 } from "simple-icons/icons";

type BrandIconProps = React.ComponentProps<"svg">;
type SimpleIconData = {
  path: string;
};

function createSimpleIcon(icon: SimpleIconData, displayName: string) {
  const Component = forwardRef<SVGSVGElement, BrandIconProps>((props, ref) => (
    <svg ref={ref} viewBox="0 0 24 24" fill="currentColor" {...props}>
      <path d={icon.path} />
    </svg>
  ));
  Component.displayName = displayName;
  return Component;
}

export const OpenAIIcon = forwardRef<SVGSVGElement, BrandIconProps>(
  (props, ref) => (
    <svg ref={ref} viewBox="0 0 24 24" fill="none" {...props}>
      <path
        d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
        fill="currentColor"
      />
    </svg>
  )
);

export const ClaudeIcon = createSimpleIcon(siClaude, "Claude Icon");
export const PerplexityIcon = createSimpleIcon(siPerplexity, "Perplexity Icon");
export const V0Icon = createSimpleIcon(siV0, "v0 Icon");

export const GrokIcon = forwardRef<SVGSVGElement, BrandIconProps>(
  (props, ref) => (
    <svg
      ref={ref}
      xmlns="http://www.w3.org/2000/svg"
      viewBox="68 74 376 362"
      fill="currentColor"
      {...props}
    >
      <path d="M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z" />
    </svg>
  )
);

More from 8starlabs-ui

Browse all components in this library →