{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-buttons",
  "type": "registry:ui",
  "title": "Share Buttons",
  "description": "A compact \"share this page\" cluster: one ghost icon-button per social network plus a copy-link affordance. Each share button opens its network's prefilled share URL in a new tab; the copy button writes the URL to the clipboard and flips its icon +…",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "blog",
    "pattern"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/button.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/stack.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/patterns/share-buttons.tsx",
      "target": "components/ui/patterns/share-buttons.tsx",
      "type": "registry:ui",
      "content": "'use client';\n\nimport * as React from 'react';\n\n// @interlace/share-buttons v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/share-buttons\n// What changed since: https://ds.interlace.tools/c/share-buttons#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — ShareButtons\n *\n * A compact \"share this page\" cluster: one ghost icon-button per social\n * network plus a copy-link affordance. Each share button opens its\n * network's prefilled share URL in a new tab; the copy button writes the\n * URL to the clipboard and flips its icon + aria-label to a brief\n * confirmation state for 1.5s before resetting.\n *\n * Client component — `navigator.clipboard.writeText` and the timed\n * confirmation toggle both require a browser runtime (R25).\n *\n * ## Anatomy\n *\n *   ShareButtons                       (Cluster — data-min-viewport=320)\n *     ├─ <a>   Button variant=ghost size=sm  (twitter — lucide Send icon)\n *     ├─ <a>   Button variant=ghost size=sm  (bluesky — lucide Cloud icon)\n *     ├─ <a>   Button variant=ghost size=sm  (linkedin — lucide Briefcase icon)\n *     └─ <button> Button variant=ghost size=sm  (copy — Copy / Check icon)\n *\n * The three network glyphs are generic lucide icons, not the networks' own\n * brand marks — lucide ships no Twitter/X, Bluesky or LinkedIn logo, and the\n * `aria-label` (`\"Share on Twitter\"`) is what actually names each button.\n *\n * ## MIN_VIEWPORT — 320\n *\n * Share-on-mobile is the dominant share path. The ghost `size=sm` button\n * meets the WCAG 2.5.5 target-size floor on a 320 CSS-px iPhone SE, and\n * the cluster wraps so four buttons still fit on one row at that width.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends React.ComponentProps     | `React.ComponentProps<'div'> & ShareButtonsProps`           |\n * | R6   | data-slot on root                | `data-slot=\"share-buttons\"`                                 |\n * | R7   | className merged + ...rest       | `cn(...) + {...props}` on the Cluster root                  |\n * | R8   | No isXxx; enums for variants     | `networks` is a string-union array, not booleans            |\n * | R10  | Composition seam                 | Built on Button + Cluster primitives                        |\n * | R13  | Ecosystem first                  | lucide-react icons; native `<a target=\"_blank\">`            |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline style; primitives own all visual classes        |\n * | R19  | Tokens only                      | Spacing/typography inherited from Cluster + Button          |\n * | R20  | AA contrast                      | Ghost Button hover/focus uses semantic tokens               |\n * | R25  | Client component                 | `'use client'` — clipboard + `useState` toggle              |\n * | R26  | A11y from native el              | `aria-label` per button; copy state announces via aria-live |\n */\n\nimport { Check, Cloud, Copy, Briefcase, Send } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\nimport { Cluster } from '@/components/ui/stack';\n\nexport const MIN_VIEWPORT = 320 as const;\n\nexport type ShareNetwork = 'twitter' | 'bluesky' | 'linkedin' | 'copy';\n\nconst DEFAULT_NETWORKS: readonly ShareNetwork[] = [\n  'twitter',\n  'bluesky',\n  'linkedin',\n  'copy',\n];\n\nconst COPIED_RESET_MS = 1500;\n\ntype ShareButtonsProps = React.ComponentProps<'div'> & {\n  /** The canonical URL being shared. */\n  url: string;\n  /** Page / post title — used as prefilled share text. */\n  title: string;\n  /**\n   * Which networks to render and in what order. Defaults to all four:\n   * `['twitter', 'bluesky', 'linkedin', 'copy']`.\n   */\n  networks?: ReadonlyArray<ShareNetwork>;\n};\n\nfunction buildShareHref(network: Exclude<ShareNetwork, 'copy'>, url: string, title: string): string {\n  const u = encodeURIComponent(url);\n  const t = encodeURIComponent(title);\n  switch (network) {\n    case 'twitter':\n      return `https://twitter.com/intent/tweet?url=${u}&text=${t}`;\n    case 'bluesky':\n      return `https://bsky.app/intent/compose?text=${t}%20${u}`;\n    case 'linkedin':\n      return `https://www.linkedin.com/sharing/share-offsite/?url=${u}`;\n  }\n}\n\nconst NETWORK_META: Record<\n  Exclude<ShareNetwork, 'copy'>,\n  { label: string; Icon: React.ComponentType<{ className?: string; 'aria-hidden'?: boolean }> }\n> = {\n  twitter: { label: 'Share on Twitter', Icon: Send },\n  bluesky: { label: 'Share on Bluesky', Icon: Cloud },\n  linkedin: { label: 'Share on LinkedIn', Icon: Briefcase },\n};\n\nexport function ShareButtons({\n  className,\n  url,\n  title,\n  networks = DEFAULT_NETWORKS,\n  ...props\n}: ShareButtonsProps) {\n  const [copied, setCopied] = React.useState(false);\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  React.useEffect(() => {\n    return () => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n    };\n  }, []);\n\n  const handleCopy = React.useCallback(async () => {\n    if (typeof navigator === 'undefined' || !navigator.clipboard) return;\n    try {\n      await navigator.clipboard.writeText(url);\n      setCopied(true);\n      if (timerRef.current) clearTimeout(timerRef.current);\n      timerRef.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);\n    } catch {\n      // Silently no-op on permission denial; consumer can wrap to show a toast.\n    }\n  }, [url]);\n\n  return (\n    <Cluster\n      gap=\"xs\"\n      align=\"center\"\n      data-slot=\"share-buttons\"\n      data-min-viewport={String(MIN_VIEWPORT)}\n      className={cn(className)}\n      {...props}\n    >\n      {networks.map((network) => {\n        if (network === 'copy') {\n          const CopyIcon = copied ? Check : Copy;\n          const label = copied ? 'Copied!' : 'Copy link';\n          return (\n            <Button\n              key=\"copy\"\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={label}\n              aria-live=\"polite\"\n              data-slot=\"share-copy\"\n              data-copied={copied ? 'true' : undefined}\n              onClick={handleCopy}\n            >\n              <CopyIcon className=\"size-4\" aria-hidden />\n            </Button>\n          );\n        }\n        const { label, Icon } = NETWORK_META[network];\n        return (\n          <Button\n            key={network}\n            variant=\"ghost\"\n            size=\"sm\"\n            aria-label={label}\n            data-slot={`share-${network}`}\n            render={\n              <a\n                href={buildShareHref(network, url, title)}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              />\n            }\n          >\n            <Icon className=\"size-4\" aria-hidden />\n          </Button>\n        );\n      })}\n    </Cluster>\n  );\n}\n"
    }
  ],
  "meta": {
    "tier": "pattern",
    "client": true,
    "minViewport": 320,
    "loading": false,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/share-buttons\n\nInstalled to `components/ui/patterns/share-buttons.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/patterns/share-buttons';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/share-buttons\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
