{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-button",
  "title": "Share Button",
  "description": "A share control that opens the device's native share sheet and quietly degrades to copying the link where that sheet does not exist. Reach for it wherever a page is worth passing on: an article or blog post, a product or listing, an invite or referral link, a public dashboard or report, a job posting, an event page, a receipt or order confirmation, a shared document or file link, a playlist or video, a profile, a support ticket a user wants to forward, or a generated QR code's target URL. Common asks it answers: 'share button react', 'web share api react', 'navigator.share button', 'native share sheet shadcn', 'share to whatsapp twitter react', 'share button with copy fallback', 'react-share alternative', 'shadcn share component'. shadcn/ui has nothing that touches navigator.share — not in button, not anywhere in its sixty-odd components — so this is written inline every time, and the inline version gets three things wrong that only show up on someone else's device. First, it assumes the API is there. navigator.share is absent on desktop Firefox, absent on any insecure origin, and present or missing on desktop Chrome depending on the OS, so the button has to have a real answer for 'no sheet' rather than throwing: here an unsupported browser, a blocked payload, or any non-cancel rejection falls through to navigator.clipboard.writeText and the label says 'Link copied', the same feedback pulld copy-button gives. Second, it awaits something first. navigator.share only resolves while the click is still the active user gesture, so building the URL or fetching a short link before calling it makes the share reject with NotAllowedError on a button that worked fine in development; this component calls share synchronously as the first thing in the handler and takes an already-resolved url prop, so pass a URL you have, not a promise. Third, it reports a cancel as an error. Dismissing the sheet rejects with AbortError, and a naive catch shows 'Sharing failed' to someone who simply closed it — here AbortError resets the button silently and reports 'cancelled' to onShare instead. The API: url (defaults to the current page URL, read at click time), shareTitle and shareText for the sheet (the native title attribute stays a tooltip), children for the resting label, timeout (default 2000ms) for how long the result state stays, and onShare(outcome) with 'shared' | 'copied' | 'cancelled' | 'failed' for analytics. Feature detection never changes what is rendered, only what the click does, so the server and the first client render agree and there is no hydration mismatch. type='button' so it will not submit a surrounding form, the result is announced through an sr-only aria-live region rather than through the icon swap, both icons are aria-hidden, and the reset timer is cleared on unmount. Styled with shadcn tokens (input, accent, ring) so it follows light and dark mode, className merges rather than fights, and the focus ring is focus-visible. One file, two lucide icons, no share library.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/share-button.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Share2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** What actually happened when the button was pressed. */\ntype ShareOutcome = \"shared\" | \"copied\" | \"cancelled\" | \"failed\"\n\ninterface ShareButtonProps\n  extends Omit<React.ComponentPropsWithoutRef<\"button\">, \"children\"> {\n  /** The URL to share. Defaults to the current page URL, read when pressed. */\n  url?: string\n  /** Title passed to the share sheet. The native `title` attribute is untouched. */\n  shareTitle?: string\n  /** Body text passed to the share sheet, shown by some targets beside the URL. */\n  shareText?: string\n  /** Label shown while the button is at rest. */\n  children?: React.ReactNode\n  /** How long (ms) the result state stays before resetting. */\n  timeout?: number\n  /** Called with the outcome, after the share sheet closes or the copy resolves. */\n  onShare?: (outcome: ShareOutcome) => void\n}\n\nexport function ShareButton({\n  url,\n  shareTitle,\n  shareText,\n  children = \"Share\",\n  timeout = 2000,\n  onShare,\n  onClick,\n  className,\n  ...props\n}: ShareButtonProps) {\n  const [status, setStatus] = React.useState<\"idle\" | ShareOutcome>(\"idle\")\n\n  React.useEffect(() => {\n    if (status === \"idle\") return\n    const id = window.setTimeout(() => setStatus(\"idle\"), timeout)\n    return () => window.clearTimeout(id)\n  }, [status, timeout])\n\n  async function copy(href: string) {\n    try {\n      await navigator.clipboard.writeText(href)\n      setStatus(\"copied\")\n      onShare?.(\"copied\")\n    } catch {\n      setStatus(\"failed\")\n      onShare?.(\"failed\")\n    }\n  }\n\n  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n    onClick?.(event)\n    if (event.defaultPrevented) return\n\n    const href = url ?? window.location.href\n    const data: ShareData = { url: href }\n    if (shareTitle) data.title = shareTitle\n    if (shareText) data.text = shareText\n\n    // Called with no `await` ahead of it: `navigator.share` only resolves while\n    // the click is still the active user gesture, so anything awaited first\n    // (building the URL, hitting an API) makes it reject with NotAllowedError.\n    if (\n      typeof navigator.share === \"function\" &&\n      (typeof navigator.canShare !== \"function\" || navigator.canShare(data))\n    ) {\n      navigator.share(data).then(\n        () => {\n          setStatus(\"shared\")\n          onShare?.(\"shared\")\n        },\n        (error: unknown) => {\n          // Dismissing the sheet rejects with AbortError. That is not a failure\n          // and must not show one: the user closed it on purpose.\n          if (error instanceof Error && error.name === \"AbortError\") {\n            onShare?.(\"cancelled\")\n            return\n          }\n          void copy(href)\n        }\n      )\n      return\n    }\n\n    void copy(href)\n  }\n\n  const message =\n    status === \"shared\"\n      ? \"Shared\"\n      : status === \"copied\"\n        ? \"Link copied\"\n        : status === \"failed\"\n          ? \"Could not share\"\n          : null\n\n  return (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      className={cn(\n        \"inline-flex h-9 items-center justify-center gap-2 rounded-md border border-input bg-transparent px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {/* The icon never depends on feature detection, so the server and the\n          first client render agree and nothing has to hydrate twice. */}\n      {status === \"shared\" || status === \"copied\" ? (\n        <Check className=\"h-4 w-4\" aria-hidden=\"true\" />\n      ) : (\n        <Share2 className=\"h-4 w-4\" aria-hidden=\"true\" />\n      )}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {message}\n      </span>\n      <span aria-hidden={message ? true : undefined}>{message ?? children}</span>\n    </button>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 103 components: https://pulld.pages.dev/?utm_source=cli"
}
