shadimage
glasscn
uicode-block-commandsource: ok · hit

Code Block Command

npx shadcn add "https://glasscn-components.vercel.app/r/code-block-command.json"
free

Visual style

terminal-mono

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

Dependencies

Bundle weight · medium
iconslucide-react
animationmotion

Registry dependencies

@glasscn/utils@glasscn/button@glasscn/badge

Files5 · source cached 10/9/2026, 6:44:56 PM

components/ui/code-block-command.tsx
"use client";

import * as React from "react";
import { WandSparklesIcon } from "lucide-react";

import { CopyButton } from "@/components/custom/copy-button";
import { type Badge } from "@/components/ui/badge";
import { PackageManagerIcon } from "@/components/ui/icons/package-manager-icons";
import { cn } from "@/lib/utils";
import type { PackageManager } from "@/lib/package-manager";

export type { PackageManager };
export { convertNpmCommand } from "@/lib/package-manager";

// ─── Context ─────────────────────────────────────────────────────────────────

type PackageManagerContextValue = {
  packageManager: PackageManager;
  setPackageManager: (pm: PackageManager) => void;
};

const PackageManagerContext = React.createContext<PackageManagerContextValue>({
  packageManager: "npm",
  setPackageManager: () => { },
});

export function PackageManagerProvider({
  children,
  defaultPackageManager = "npm",
}: {
  children: React.ReactNode;
  defaultPackageManager?: PackageManager;
}) {
  const [packageManager, setPackageManagerState] =
    React.useState<PackageManager>(() => {
      if (typeof window !== "undefined") {
        return (
          (localStorage.getItem("packageManager") as PackageManager) ??
          defaultPackageManager
        );
      }
      return defaultPackageManager;
    });

  const setPackageManager = React.useCallback((pm: PackageManager) => {
    setPackageManagerState(pm);
    localStorage.setItem("packageManager", pm);
  }, []);

  return (
    <PackageManagerContext.Provider value={{ packageManager, setPackageManager }}>
      {children}
    </PackageManagerContext.Provider>
  );
}

export function usePackageManager() {
  return React.useContext(PackageManagerContext);
}

type ActiveTab = PackageManager | "prompt";

export type CodeBlockCommandProps = {
  prompt?: string;
  npm?: string;
  pnpm?: string;
  yarn?: string;
  bun?: string;
  onCopySuccess?: (text: string) => void;
  onCopyError?: (error: Error) => void;
  className?: string;
  badgeComponent?: React.ComponentType<React.ComponentProps<typeof Badge>>;
};

const PACKAGE_MANAGERS: PackageManager[] = ["pnpm", "yarn", "npm", "bun"];

export function CodeBlockCommand({
  prompt,
  npm,
  pnpm,
  yarn,
  bun,
  onCopySuccess,
  onCopyError,
  className,
  badgeComponent: BadgeComponent,
}: CodeBlockCommandProps) {
  const { packageManager, setPackageManager } = usePackageManager();

  const commands: Record<PackageManager, string | undefined> = { npm, pnpm, yarn, bun };

  const availablePMs = PACKAGE_MANAGERS.filter((pm) => commands[pm] !== undefined);

  // Local tab state — "prompt" tab doesn't affect the shared PM context
  const [activeTab, setActiveTab] = React.useState<ActiveTab>(() => {
    if (availablePMs.includes(packageManager)) return packageManager;
    return availablePMs[0] ?? "npm";
  });

  // Keep active tab in sync when the shared PM context changes
  React.useEffect(() => {
    if (availablePMs.includes(packageManager)) {
      setActiveTab(packageManager);
    }
  }, [packageManager]); // eslint-disable-line react-hooks/exhaustive-deps

  function handleTabClick(tab: ActiveTab) {
    setActiveTab(tab);
    if (tab !== "prompt") {
      setPackageManager(tab);
    }
  }

  const activeContent =
    activeTab === "prompt" ? (prompt ?? "") : (commands[activeTab] ?? "");

  return (
    <div
      data-slot="code-block-command"
      className={cn("relative overflow-hidden rounded-xl border bg-muted", className)}
    >
      <div data-slot="code-block-command-header" className="flex items-center border-b px-4">
        {BadgeComponent ? (
          <BadgeComponent
            data-slot="code-block-command-active-icon"
            variant="outline"
            className="mr-3 size-6 shrink-0 rounded-full p-0"
          >
            {activeTab === "prompt" ? (
              <WandSparklesIcon aria-hidden className="size-3.5" />
            ) : (
              <PackageManagerIcon packageManager={activeTab} className="size-3.5" />
            )}
          </BadgeComponent>
        ) : activeTab === "prompt" ? (
          <WandSparklesIcon
            aria-hidden
            data-slot="code-block-command-active-icon"
            className="mr-3 size-5 shrink-0 text-muted-foreground"
          />
        ) : (
          <PackageManagerIcon
            packageManager={activeTab}
            data-slot="code-block-command-active-icon"
            className="mr-3 size-5 shrink-0 text-muted-foreground"
          />
        )}

        {prompt && (
          <TabButton active={activeTab === "prompt"} onClick={() => handleTabClick("prompt")}>
            prompt
          </TabButton>
        )}

        {availablePMs.map((pm) => (
          <TabButton key={pm} active={activeTab === pm} onClick={() => handleTabClick(pm)}>
            {pm}
          </TabButton>
        ))}
      </div>

      <div data-slot="code-block-command-content" className="px-4 py-3 pr-12">
        <pre className="overflow-x-auto overscroll-x-contain">
          <code
            data-slot="code-block-command-code"
            className="font-mono text-sm leading-none text-foreground/80"
          >
            {activeTab !== "prompt" && <span className="select-none">$ </span>}
            {activeContent}
          </code>
        </pre>
      </div>

      <CopyButton
        data-slot="code-block-command-copy"
        size="icon-sm"
        className="absolute top-2 right-2"
        text={activeContent}
        onCopySuccess={onCopySuccess}
        onCopyError={onCopyError}
      />
    </div>
  );
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

function TabButton({
  active,
  onClick,
  children,
}: {
  active: boolean;
  onClick: () => void;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      data-slot="code-block-command-tab"
      data-active={active}
      className={cn(
        "h-10 border-b-2 border-transparent px-2 font-mono text-sm transition-colors",
        active
          ? "border-foreground text-foreground"
          : "text-muted-foreground hover:text-foreground",
      )}
    >
      {children}
    </button>
  );
}
components/ui/icons/package-manager-icons.tsx
import type { SVGProps } from "react";

import type { PackageManager } from "@/lib/package-manager";
import { cn } from "@/lib/utils";

// Brand colors from Simple Icons (simpleicons.org)
// npm   → #CB3837  pnpm → #F69220
// yarn  → #2C8EBB  bun  → #FBF0DF (cream — dark bg brand; inverted for light mode)

function NpmIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" {...props}>
      <path
        fill="#cb3837"
        d="M2 38.5h124v43.71H64v7.29H36.44v-7.29H2zm6.89 36.43h13.78V53.07h6.89v21.86h6.89V45.79H8.89zm34.44-29.14v36.42h13.78v-7.28h13.78V45.79zm13.78 7.29H64v14.56h-6.89zm20.67-7.29v29.14h13.78V53.07h6.89v21.86h6.89V53.07h6.89v21.86h6.89V45.79z"
      />
    </svg>
  );
}

function PnpmIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
      <path d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z" />
    </svg>
  );
}

function YarnIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" {...props}>
      <g fill="#2c8ebb">
        <path d="M99.24 80.71C94.9 80.76 91.1 83 87.89 85c-6 3.71-9 3.47-9 3.47l-.1-.17c-.41-.67 1.92-6.68-.69-13.84-2.82-7.83-7.3-9.72-6.94-10.32 1.53-2.59 5.36-6.7 6.89-14.36.91-4.64.67-12.28-1.39-16.28-.38-.74-3.78 1.24-3.78 1.24s-3.18-7.09-4.07-7.66c-2.87-1.84-6 7.61-6 7.61a14 14 0 00-11.71 4.5 9.64 9.64 0 01-3.85 2.27c-.41.14-.91.12-2.15 3.47-1.9 5.07 3.24 10.81 3.24 10.81s-6.13 4.33-8.4 9.72a24.78 24.78 0 00-1.75 11.68s-4.36 3.78-4.64 7.68a12.87 12.87 0 001.77 7.83 1.94 1.94 0 002.63.91s-2.9 3.38-.19 4.81c2.47 1.29 6.63 2 8.83-.19 1.6-1.6 1.92-5.17 2.51-6.63.14-.34.62.57 1.08 1a10 10 0 001.36 1s-3.9 1.68-2.3 5.51c.53 1.27 2.42 2.08 5.51 2.06 1.15 0 13.76-.72 17.12-1.53a4.33 4.33 0 002.61-1.46 63 63 0 0015.49-7c4.74-3.09 6.68-3.93 10.51-4.84 3.16-.75 2.95-5.65-1.24-5.58z" />
        <path d="M64 2a62 62 0 1062 62A62 62 0 0064 2zm37.3 87.83c-3.35.81-4.91 1.44-9.41 4.36a67 67 0 01-15.56 7.18 8.71 8.71 0 01-3.64 1.77c-3.81.93-16.88 1.63-17.91 1.63h-.24c-4 0-6.27-1.24-7.49-2.54-3.4 1.7-7.8 1-11-.69a5.55 5.55 0 01-3-3.9 6 6 0 010-2.06 6.66 6.66 0 01-.79-1A16.38 16.38 0 0130 84.52c.29-3.73 2.87-7.06 4.55-8.83A28.56 28.56 0 0136.61 64a26.82 26.82 0 016.82-9c-1.65-2.78-3.33-7.06-1.7-11.42 1.17-3.11 2.13-4.84 4.24-5.58a6.84 6.84 0 002.51-1.34A17.65 17.65 0 0160.34 31c.19-.48.41-1 .65-1.46 1.6-3.4 3.3-5.31 5.29-6a4.88 4.88 0 014.4.5c.65.43 1.48 1 3.9 6a4.69 4.69 0 012.85-.1 3.81 3.81 0 012.39 1.94c2.47 4.74 2.8 13.19 1.72 18.62a33.8 33.8 0 01-5.84 13.31 25.73 25.73 0 015.77 9.43 25.42 25.42 0 011.41 10.41A28.7 28.7 0 0186 81.91c3.06-1.89 7.68-4.74 13.19-4.81a6.62 6.62 0 017 5.7 6.35 6.35 0 01-4.89 7.03z" />
      </g>
    </svg>
  );
}

function BunIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" {...props}>
      <path d="M113.744 41.999a18.558 18.558 0 0 0-.8-.772c-.272-.246-.528-.524-.8-.771s-.528-.525-.8-.771c-.272-.247-.528-.525-.8-.772s-.528-.524-.8-.771-.528-.525-.8-.772-.528-.524-.8-.771c7.936 7.52 12.483 17.752 12.656 28.481 0 25.565-26.912 46.363-60 46.363-18.528 0-35.104-6.526-46.128-16.756l.8.772.8.771.8.772.8.771.8.772.8.771.8.771c11.008 10.662 27.952 17.527 46.928 17.527 33.088 0 60-20.797 60-46.285 0-10.893-4.864-21.215-13.456-29.33z" />
      <path
        fill="#fbf0df"
        d="M116.8 65.08c0 23.467-25.072 42.49-56 42.49s-56-19.023-56-42.49c0-14.55 9.6-27.401 24.352-35.023C43.904 22.435 53.088 14.628 60.8 14.628S75.104 21 92.448 30.058C107.2 37.677 116.8 50.53 116.8 65.08Z"
      />
      <path
        fill="#f6dece"
        d="M116.8 65.08a32.314 32.314 0 0 0-1.28-8.918c-4.368 51.377-69.36 53.846-94.912 38.48 11.486 8.584 25.66 13.144 40.192 12.928 30.88 0 56-19.054 56-42.49z"
      />
      <path
        fill="#fffefc"
        d="M39.248 27.234c7.152-4.135 16.656-11.896 26-11.911a15.372 15.372 0 0 0-4.448-.695c-3.872 0-8 1.93-13.2 4.83-1.808 1.018-3.68 2.144-5.664 3.317-3.728 2.222-8 4.736-12.8 7.251C13.904 37.972 4.8 51.071 4.8 65.08v1.836c9.696-33.033 27.312-35.547 34.448-39.682z"
      />
      <path
        fill="#ccbea7"
        d="M56.192 18.532A24.553 24.553 0 0 1 53.867 29.1a25.407 25.407 0 0 1-6.683 8.671c-.448.386-.096 1.127.48.91 5.392-2.02 12.672-8.068 9.6-20.272-.128-.695-1.072-.51-1.072.123zm3.632 0a24.474 24.474 0 0 1 3.646 10.12c.445 3.587.08 7.224-1.07 10.662-.192.54.496 1.003.88.556 3.504-4.32 6.56-12.899-2.592-22.156-.464-.4-1.184.216-.864.756zm4.416-.262a25.702 25.702 0 0 1 7.521 7.925A24.71 24.71 0 0 1 75.2 36.414c-.016.13.02.26.101.365a.543.543 0 0 0 .718.117.509.509 0 0 0 .221-.313c1.472-5.384.64-14.564-11.472-19.332-.64-.246-1.056.587-.528.957zM34.704 34.315a27.418 27.418 0 0 0 9.91-5.222 26.262 26.262 0 0 0 6.842-8.663c.288-.556 1.2-.34 1.056.277-2.768 12.343-12.032 14.92-17.792 14.58-.608.016-.592-.802-.016-.972z"
      />
      <path d="M60.8 111.443c-33.088 0-60-20.798-60-46.363 0-15.429 9.888-29.823 26.448-38.448 4.8-2.469 8.912-4.953 12.576-7.128 2.016-1.203 3.92-2.33 5.76-3.379C51.2 12.916 56 10.771 60.8 10.771c4.8 0 8.992 1.852 14.24 4.845 1.6.88 3.2 1.836 4.912 2.885 3.984 2.376 8.48 5.06 14.4 8.131 16.56 8.625 26.448 23.004 26.448 38.448 0 25.565-26.912 46.363-60 46.363zm0-96.814c-3.872 0-8 1.928-13.2 4.829-1.808 1.018-3.68 2.144-5.664 3.317-3.728 2.222-8 4.736-12.8 7.251C13.904 37.972 4.8 51.071 4.8 65.08c0 23.436 25.12 42.506 56 42.506s56-19.07 56-42.506c0-14.01-9.104-27.108-24.352-35.023-6.048-3.086-10.768-5.986-14.592-8.27-1.744-1.033-3.344-1.99-4.8-2.838-4.848-2.778-8.384-4.32-12.256-4.32z" />
      <path
        fill="#b71422"
        d="M72.08 76.343c-.719 2.839-2.355 5.383-4.672 7.267a11.07 11.07 0 0 1-6.4 2.9 11.13 11.13 0 0 1-6.608-2.9c-2.293-1.892-3.906-4.436-4.608-7.267a1.073 1.073 0 0 1 .05-.5 1.11 1.11 0 0 1 .272-.428 1.19 1.19 0 0 1 .958-.322h19.744a1.185 1.185 0 0 1 .947.33 1.073 1.073 0 0 1 .317.92z"
      />
      <path
        fill="#ff6164"
        d="M54.4 83.733a11.24 11.24 0 0 0 6.592 2.932 11.239 11.239 0 0 0 6.576-2.932 16.652 16.652 0 0 0 1.6-1.65 10.904 10.904 0 0 0-3.538-2.564 11.26 11.26 0 0 0-4.302-1 10.121 10.121 0 0 0-4.549 1.192 9.71 9.71 0 0 0-3.451 3.097c.368.323.688.632 1.072.925z"
      />
      <path d="M54.656 82.514a8.518 8.518 0 0 1 2.97-2.347 8.836 8.836 0 0 1 3.734-.862 9.78 9.78 0 0 1 6.4 2.608c.368-.386.72-.787 1.056-1.188-2.035-1.87-4.726-2.933-7.536-2.978a10.487 10.487 0 0 0-4.335.975 10.125 10.125 0 0 0-3.489 2.666c.378.396.779.772 1.2 1.126z" />
      <path d="M60.944 87.436a12.078 12.078 0 0 1-7.12-3.086c-2.477-2.02-4.22-4.75-4.976-7.791-.054-.27-.045-.55.027-.817a1.83 1.83 0 0 1 .389-.726 2.25 2.25 0 0 1 .81-.595 2.32 2.32 0 0 1 .998-.192h19.744c.343-.007.683.06.996.196a2.3 2.3 0 0 1 .812.591c.182.212.313.46.382.728.07.267.076.545.018.815-.756 3.042-2.5 5.771-4.976 7.791a12.078 12.078 0 0 1-7.104 3.086zm-9.872-11.417c-.256 0-.32.108-.336.139.676 2.638 2.206 4.999 4.368 6.742a10.122 10.122 0 0 0 5.84 2.7 10.207 10.207 0 0 0 5.84-2.67c2.155-1.745 3.679-4.106 4.352-6.741a.333.333 0 0 0-.14-.113.348.348 0 0 0-.18-.026z" />
      <path
        fill="#febbd0"
        d="M85.152 77.3c5.17 0 9.36-2.377 9.36-5.308s-4.19-5.307-9.36-5.307c-5.17 0-9.36 2.376-9.36 5.307 0 2.931 4.19 5.307 9.36 5.307zm-48.432 0c5.17 0 9.36-2.377 9.36-5.308s-4.19-5.307-9.36-5.307c-5.17 0-9.36 2.376-9.36 5.307 0 2.931 4.19 5.307 9.36 5.307z"
      />
      <path d="M41.12 69.863a9.052 9.052 0 0 0 4.902-1.425 8.578 8.578 0 0 0 3.254-3.812 8.22 8.22 0 0 0 .508-4.913 8.41 8.41 0 0 0-2.408-4.357 8.92 8.92 0 0 0-4.514-2.33 9.12 9.12 0 0 0-5.096.48 8.755 8.755 0 0 0-3.96 3.131 8.287 8.287 0 0 0-1.486 4.725c0 2.252.927 4.412 2.577 6.005 1.65 1.594 3.888 2.492 6.223 2.496zm39.632 0a9.054 9.054 0 0 0 4.915-1.403 8.582 8.582 0 0 0 3.275-3.802 8.22 8.22 0 0 0 .528-4.917 8.408 8.408 0 0 0-2.398-4.368 8.92 8.92 0 0 0-4.512-2.344 9.12 9.12 0 0 0-5.103.473 8.756 8.756 0 0 0-3.967 3.13 8.287 8.287 0 0 0-1.49 4.73c-.004 2.245.914 4.4 2.555 5.994 1.64 1.593 3.869 2.495 6.197 2.507z" />
      <path
        fill="#fff"
        d="M38.4 61.902a3.4 3.4 0 0 0 1.844-.531c.547-.35.974-.847 1.227-1.43a3.088 3.088 0 0 0 .195-1.847 3.16 3.16 0 0 0-.902-1.639 3.351 3.351 0 0 0-1.696-.878 3.426 3.426 0 0 0-1.916.179 3.29 3.29 0 0 0-1.489 1.176 3.113 3.113 0 0 0-.559 1.776c0 .844.347 1.654.964 2.253a3.374 3.374 0 0 0 2.332.94zm39.632 0a3.4 3.4 0 0 0 1.844-.531c.547-.35.974-.847 1.227-1.43a3.088 3.088 0 0 0 .195-1.847 3.16 3.16 0 0 0-.902-1.639 3.351 3.351 0 0 0-1.696-.878 3.426 3.426 0 0 0-1.916.179 3.29 3.29 0 0 0-1.489 1.176 3.113 3.113 0 0 0-.559 1.776c0 .84.342 1.644.953 2.242.61.598 1.44.94 2.311.952z"
      />
    </svg>
  );
}

const PM_STYLES: Record<PackageManager, { style?: React.CSSProperties; className?: string }> = {
  npm: {},
  pnpm: { style: { color: "#F69220" } },
  yarn: {},
  bun: {},
};

export function PackageManagerIcon({
  packageManager,
  className,
}: {
  packageManager: PackageManager;
  className?: string;
}) {
  const { style, className: colorClass } = PM_STYLES[packageManager];
  const sharedProps = { "aria-hidden": true as const, style, className: cn(colorClass, className) };

  switch (packageManager) {
    case "npm":
      return <NpmIcon {...sharedProps} />;
    case "pnpm":
      return <PnpmIcon {...sharedProps} />;
    case "yarn":
      return <YarnIcon {...sharedProps} />;
    case "bun":
      return <BunIcon {...sharedProps} />;
  }
}
components/custom/copy-button.tsx
"use client";

import { CheckIcon, CircleXIcon, CopyIcon } from "lucide-react";
import type { HTMLMotionProps, Variants } from "motion/react";
import { AnimatePresence, motion } from "motion/react";
import type { ComponentProps } from "react";

import { Button } from "@/components/ui/button";
import type { CopyState } from "@/hooks/use-copy-to-clipboard";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";

export const motionIconVariants: Variants = {
  initial: { opacity: 0, scale: 0.8, filter: "blur(2px)" },
  animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
  exit: { opacity: 0, scale: 0.8 },
};

export const motionIconProps: HTMLMotionProps<"span"> = {
  variants: motionIconVariants,
  initial: "initial",
  animate: "animate",
  exit: "exit",
  transition: { duration: 0.25, ease: "easeOut" },
};

export type CopyStateIconProps = {
  state: CopyState;
  idleIcon?: React.ReactNode;
  doneIcon?: React.ReactNode;
  errorIcon?: React.ReactNode;
};

export function CopyStateIcon({ state, idleIcon, doneIcon, errorIcon }: CopyStateIconProps) {
  return (
    <AnimatePresence mode="wait" initial={false}>
      {state === "idle" ? (
        <motion.span key="idle" {...motionIconProps}>
          {idleIcon ?? <CopyIcon />}
        </motion.span>
      ) : state === "done" ? (
        <motion.span key="done" {...motionIconProps}>
          {doneIcon ?? <CheckIcon strokeWidth={3} color="green" />}
        </motion.span>
      ) : state === "error" ? (
        <motion.span key="error" {...motionIconProps}>
          {errorIcon ?? <CircleXIcon />}
        </motion.span>
      ) : null}
    </AnimatePresence>
  );
}

export type CopyButtonProps = ComponentProps<typeof Button> & {
  text: string | (() => string);
  onCopySuccess?: (text: string) => void;
  onCopyError?: (error: Error) => void;
} & Pick<CopyStateIconProps, "idleIcon" | "doneIcon" | "errorIcon">;

export function CopyButton({
  size = "icon",
  variant = "ghost",
  children,
  text,
  idleIcon,
  doneIcon,
  errorIcon,
  onClick,
  onCopySuccess,
  onCopyError,
  ...props
}: CopyButtonProps) {
  const { state, copy } = useCopyToClipboard({ onCopySuccess, onCopyError });

  return (
    <Button
      size={size}
      variant={variant}
      onClick={(e) => {
        void copy(text);
        onClick?.(e);
      }}
      aria-label="Copy"
      {...props}
    >
      <CopyStateIcon state={state} idleIcon={idleIcon} doneIcon={doneIcon} errorIcon={errorIcon} />
      {children}
    </Button>
  );
}
hooks/use-copy-to-clipboard.ts
import { useState } from "react";

export type CopyState = "idle" | "done" | "error";

type UseCopyToClipboardOptions = {
  onCopySuccess?: (text: string) => void;
  onCopyError?: (error: Error) => void;
  timeout?: number;
};

export function useCopyToClipboard({ onCopySuccess, onCopyError, timeout = 2000 }: UseCopyToClipboardOptions = {}) {
  const [state, setState] = useState<CopyState>("idle");

  async function copy(text: string | (() => string)) {
    const value = typeof text === "function" ? text() : text;
    try {
      await navigator.clipboard.writeText(value);
      setState("done");
      onCopySuccess?.(value);
    } catch (err) {
      setState("error");
      onCopyError?.(err instanceof Error ? err : new Error(String(err)));
    } finally {
      setTimeout(() => setState("idle"), timeout);
    }
  }

  return { state, copy };
}
lib/package-manager.ts
export type PackageManager = "pnpm" | "yarn" | "npm" | "bun";

export function convertNpmCommand(command: string): Record<PackageManager, string> {
  const pnpm = command
    .replace(/^npm install/, "pnpm add")
    .replace(/^npx create-(\S+)/, "pnpm create $1")
    .replace(/^npx/, "pnpm dlx")
    .replace(/^npm run/, "pnpm");

  const yarn = command
    .replace(/^npm install/, "yarn add")
    .replace(/^npx create-(\S+)/, "yarn create $1")
    .replace(/^npx/, "yarn dlx")
    .replace(/^npm run/, "yarn");

  const bun = command
    .replace(/^npm install/, "bun add")
    .replace(/^npx create-(\S+)/, "bunx --bun create-$1")
    .replace(/^npx/, "bunx --bun")
    .replace(/^npm run/, "bun");

  return { npm: command, pnpm, yarn, bun };
}

More from glasscn

Browse all components in this library →