A region that takes whatever is on the clipboard — a screenshot, an image, a file, a table copied out of a spreadsheet, or plain text — and hands it over as typed items, with each pasted picture given a name it can actually be filed under instead of the image.png that every browser calls it. Reach for it wherever the thing a person has is copied rather than saved: a screenshot going into a bug report, a support ticket or a QA tool, an image dropped into a chat message, a CMS entry or a comment box, an avatar or profile picture grabbed from somewhere else, a receipt or an invoice pasted into an expense claim, rows pasted out of Excel, Numbers or Google Sheets into an importer, a logo pasted into a brand or settings form, and a frame copied straight out of Figma during a design handoff. Common asks it answers: "paste image react", "paste screenshot upload react", "react paste from clipboard component", "onPaste handler react", "get image from clipboard javascript", "clipboardData.items getAsFile", "clipboardData.files is empty", "paste event has no files", "screenshot paste not working", "paste image works in chrome not safari", "navigator.clipboard.read example", "clipboard read permission denied", "NotAllowedError clipboard", "paste area shadcn", "shadcn paste component", "paste to upload", "ctrl+v upload", "cmd+v upload image", "paste excel table into react", "paste tsv into a table", "text/html vs text/plain clipboard", "pasted image has no filename", "every pasted file is called image.png", "paste anywhere on the page", "document paste listener react", "paste event only fires when focused", "paste event not firing on div", "クリップボード 貼り付け react", "スクショ 貼り付け アップロード", "画像 ペースト 受け取る". Official shadcn/ui has nothing here and nothing to build it out of: fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 223,287 bytes of source, onPaste, clipboardData, ClipboardEvent, DataTransfer, navigator.clipboard, getAsFile, ClipboardItem, clipboard.read and readText are every one of them a zero hit (measured 2026-09-29), and so is the word paste itself, in either case. It does not touch the clipboard anywhere. Distinct from the pulld pieces it sits beside, which all face the other way: copy-button and copy-field write to the clipboard, file-dropzone receives a file that already exists on disk by drag or by picker, camera-capture makes a new picture, image-crop and image-zoom work on a picture you already hold, and upload-list shows them afterwards. This is the only one that reads. It is deliberately stateless too — it hands each paste on and keeps no list, so it composes with whichever of those holds the files. Seven things separate it from the version built out of onPaste and e.clipboardData.files. First, the two clipboard mechanisms are available under opposite conditions, so both are wired: the paste event needs no permission and works everywhere but only ever reaches the focused element — which is why the region is focusable and why a global prop exists for a screen whose whole job is to receive one thing — while navigator.clipboard.read() can be called at any moment but prompts for permission, is not implemented everywhere, and in some engines only resolves inside a user gesture, which is why it sits behind a button. Second, that button appears only after mount: the server has no navigator, so deciding it during render puts a button in the client's first tree that is missing from the server's HTML, and React throws the whole tree away rather than reconciling that. Third, files and items disagree about the most common paste there is. A screenshot is a picture that was never a file on disk, so it turns up in items and, in some browsers, nowhere else — read files alone and the single thing people paste most is silently ignored. Both are read and then de-duplicated, because an ordinary copied file is usually in both; and the identity used for that is name, size and type but deliberately not lastModified, since getAsFile() mints a fresh File on every call and some engines stamp it with the current millisecond, so the two reads of one picture differ by a millisecond and the person is handed their screenshot twice. Fourth, one paste carries several flavours at once and choosing between them is not this component's call: a spreadsheet selection arrives as a real table in text/html and as tab-separated text in text/plain, and which one is read changes the result entirely, so both are handed over along with the full list of types. The strings are read with getData, synchronously, rather than with items[i].getAsString, whose callback runs on a later turn — by which time the event's DataTransfer has been emptied and it fires with nothing. Fifth, every pasted screenshot is called image.png. One is fine; the second lands in the list beside a file of the same name, and from there "which image.png?" is a question the UI cannot answer and an upload keyed on filename quietly overwrites. Placeholders are renamed with a timestamp and a sequence that continues across pastes rather than restarting, so two pastes inside the same second cannot collide either, and the original name is reported back. A file that came with a real name keeps it — photo.jpg is deliberately not on the list of placeholders, because renaming it would destroy the only information its owner supplied. Sixth, a paste that belongs to a text box is left to it. A region holding a caption field, or one listening on the document, is handed pastes meant for an input, and swallowing one means someone watches their text simply not appear; a read-only or disabled field is not one of those, since nothing can be pasted into it, so the region takes that one. Seventh, in global mode the element's own handler is dropped rather than added to, because a paste event bubbles to the document as well — keeping both delivers one Ctrl+V twice, as two copies of the same screenshot. Files are screened with accept, maxSize and maxFiles and anything skipped is reported through onReject instead of vanishing; every stage is announced through a live region that is in the DOM before it has anything to say, and visibly rather than only to screen readers, because "clipboard permission denied" is the answer to "why did nothing happen" and sighted people ask that too. One file, shadcn tokens throughout so it follows light and dark themes, and lucide-react is the only dependency.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/paste-area.json"