# UploadThing Paste

> A clipboard paste zone that accepts pasted images and files. Perfect for quick screenshot uploads.

A shadcn/ui block from the [@elements](https://www.tryelements.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/elements/uploadthing-paste).

- Page, with a live preview: https://shadcn.exchange/r/elements/uploadthing-paste
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/elements/uploadthing-paste-light-0e4d7e4bc5ea9c96.png
- Registry item: https://www.tryelements.dev/r/uploadthing-paste.json
- Categories: uploadthing
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.tryelements.dev/r/uploadthing-paste.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@uploadthing/react`, `uploadthing`

## Notes from the registry

Focus the zone and paste (Cmd+V) to upload. Shows upload progress and success state. Supports file type filtering via accept prop.

## Files

### `registry/default/blocks/uploadthing/uploadthing-paste/components/elements/uploadthing-paste.tsx`

```tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";

interface UploadThingPasteProps {
  onPaste?: (files: File[]) => void;
  onUpload?: (files: File[]) => Promise<void>;
  accept?: string[];
  className?: string;
  disabled?: boolean;
  children?: React.ReactNode;
}

function ClipboardIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
      <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
    </svg>
  );
}

function ImageIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
      <circle cx="9" cy="9" r="2" />
      <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
    </svg>
  );
}

function LoadingSpinner({ className }: { className?: string }) {
  return (
    <svg
      className={cn("animate-spin", className)}
      viewBox="0 0 24 24"
      fill="none"
    >
      <circle
        className="opacity-25"
        cx="12"
        cy="12"
        r="10"
        stroke="currentColor"
        strokeWidth="4"
      />
      <path
        className="opacity-75"
        fill="currentColor"
        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
      />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <polyline points="20 6 9 17 4 12" />
    </svg>
  );
}

export function UploadThingPaste({
  onPaste,
  onUpload,
  accept = ["image/*"],
  className,
  disabled = false,
  children,
}: UploadThingPasteProps) {
  const [isFocused, setIsFocused] = useState(false);
  const [isUploading, setIsUploading] = useState(false);
  const [lastPasted, setLastPasted] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);

  const handlePaste = useCallback(
    async (e: ClipboardEvent) => {
      if (disabled || isUploading) return;

      const items = e.clipboardData?.items;
      if (!items) return;

      const files: File[] = [];
      for (const item of items) {
        if (item.kind === "file") {
          const file = item.getAsFile();
          if (file) {
            const isAccepted = accept.some((pattern) => {
              if (pattern === "*" || pattern === "*/*") return true;
              if (pattern.endsWith("/*")) {
                return file.type.startsWith(pattern.replace("/*", "/"));
              }
              return file.type === pattern;
            });
            if (isAccepted) {
              files.push(file);
            }
          }
        }
      }

      if (files.length === 0) return;

      e.preventDefault();
      onPaste?.(files);

      if (files[0]) {
        setLastPasted(files[0].name);
      }

      if (onUpload) {
        try {
          setIsUploading(true);
          await onUpload(files);
          setSuccess(true);
          setTimeout(() => setSuccess(false), 2000);
        } catch (err) {
          console.error("Upload failed:", err);
        } finally {
          setIsUploading(false);
        }
      }
    },
    [accept, disabled, isUploading, onPaste, onUpload]
  );

  useEffect(() => {
    const container = containerRef.current;
    if (!container || !isFocused) return;

    const handleGlobalPaste = (e: ClipboardEvent) => {
      if (document.activeElement === container || container.contains(document.activeElement)) {
        handlePaste(e);
      }
    };

    document.addEventListener("paste", handleGlobalPaste);
    return () => document.removeEventListener("paste", handleGlobalPaste);
  }, [isFocused, handlePaste]);

  const getStatusContent = () => {
    if (isUploading) {
      return (
        <>
          <LoadingSpinner className="w-8 h-8 text-primary" />
          <span className="text-sm text-muted-foreground">Uploading...</span>
        </>
      );
    }

    if (success) {
      return (
        <>
          <div className="w-12 h-12 rounded-full bg-green-500/10 flex items-center justify-center">
            <CheckIcon className="w-6 h-6 text-green-500" />
          </div>
          <span className="text-sm text-green-600 dark:text-green-400">
            Uploaded successfully!
          </span>
        </>
      );
    }

    if (lastPasted) {
      return (
        <>
          <ImageIcon className="w-8 h-8 text-muted-foreground" />
          <span className="text-sm text-muted-foreground truncate max-w-[200px]">
            {lastPasted}
          </span>
        </>
      );
    }

    return (
      <>
        <ClipboardIcon className="w-8 h-8 text-muted-foreground" />
        <span className="text-sm text-muted-foreground">
          {isFocused ? (
            <span>
              Press <kbd className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">⌘V</kbd> to paste
            </span>
          ) : (
            "Click to focus, then paste"
          )}
        </span>
      </>
    );
  };

  if (children) {
    return (
      <div
        ref={containerRef}
        data-slot="uploadthing-paste"
        tabIndex={disabled ? -1 : 0}
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        className={cn(
          "outline-none",
          isFocused && "ring-2 ring-primary ring-offset-2 rounded-lg",
          className
        )}
      >
        {children}
      </div>
    );
  }

  return (
    <div
      ref={containerRef}
      data-slot="uploadthing-paste"
      tabIndex={disabled ? -1 : 0}
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
      className={cn(
        "flex flex-col items-center justify-center gap-3 p-8",
        "border-2 border-dashed rounded-lg transition-all cursor-pointer",
        "outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
        isFocused ? "border-primary bg-primary/5" : "border-border hover:border-primary/50",
        disabled && "opacity-50 cursor-not-allowed",
        className
      )}
    >
      {getStatusContent()}
    </div>
  );
}
```
