{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-bar",
  "title": "Share Bar",
  "author": "ilinxa",
  "description": "Social share button row — nine built-in platforms, custom targets, and copy-link with success feedback.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/components/marketing/share-bar/share-bar.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ShareButton, type CopyState } from \"./parts/share-button\";\nimport { SHARE_TEMPLATES } from \"./parts/templates\";\nimport {\n  SHARE_BAR_DEFAULT_LABELS,\n  type ShareBarProps,\n  type ShareTarget,\n} from \"./types\";\n\nasync function copyToClipboard(text: string): Promise<void> {\n  if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n    return navigator.clipboard.writeText(text);\n  }\n  if (typeof document === \"undefined\") {\n    throw new Error(\"Clipboard not available (no document)\");\n  }\n  const textarea = document.createElement(\"textarea\");\n  textarea.value = text;\n  textarea.style.position = \"fixed\";\n  textarea.style.opacity = \"0\";\n  document.body.appendChild(textarea);\n  textarea.select();\n  try {\n    if (!document.execCommand(\"copy\")) {\n      throw new Error(\"execCommand copy returned false\");\n    }\n  } finally {\n    document.body.removeChild(textarea);\n  }\n}\n\nfunction ShareBarInner(props: ShareBarProps) {\n  const {\n    targets,\n    url,\n    title,\n    text,\n    via,\n    hashtags,\n    onShare,\n    onCopySuccess,\n    onCopyError,\n    successResetMs = 2000,\n    divider = false,\n    headingAs,\n    labels,\n    className,\n    headerClassName,\n    buttonClassName,\n  } = props;\n\n  const HeadingTag = headingAs;\n  const resolvedLabels = { ...SHARE_BAR_DEFAULT_LABELS, ...labels };\n  const headingId = useId();\n\n  const [copyState, setCopyState] = useState<CopyState>(\"idle\");\n  const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(\n    () => () => {\n      if (resetTimer.current !== null) {\n        clearTimeout(resetTimer.current);\n      }\n    },\n    []\n  );\n\n  const scheduleReset = useCallback((ms: number) => {\n    if (resetTimer.current !== null) {\n      clearTimeout(resetTimer.current);\n    }\n    resetTimer.current = setTimeout(() => setCopyState(\"idle\"), ms);\n  }, []);\n\n  const resolveUrl = useCallback(() => {\n    if (url) return url;\n    return typeof window !== \"undefined\" ? window.location.href : \"\";\n  }, [url]);\n\n  const handleClick = useCallback(\n    (target: ShareTarget) => async () => {\n      if (target.kind === \"copy\") {\n        try {\n          await copyToClipboard(resolveUrl());\n          setCopyState(\"success\");\n          onCopySuccess?.();\n          onShare?.(\"copy\");\n          scheduleReset(successResetMs);\n        } catch (err) {\n          setCopyState(\"error\");\n          onCopyError?.(err);\n          scheduleReset(successResetMs);\n        }\n        return;\n      }\n\n      if (target.kind === \"custom\") {\n        target.onClick();\n        onShare?.(target.id);\n        return;\n      }\n\n      const shareUrl = SHARE_TEMPLATES[target.kind]({\n        url: resolveUrl(),\n        title,\n        text,\n        via,\n        hashtags,\n      });\n      if (typeof window !== \"undefined\") {\n        window.open(shareUrl, \"_blank\", \"noopener,noreferrer\");\n      }\n      onShare?.(target.kind);\n    },\n    [\n      resolveUrl,\n      title,\n      text,\n      via,\n      hashtags,\n      onShare,\n      onCopySuccess,\n      onCopyError,\n      successResetMs,\n      scheduleReset,\n    ]\n  );\n\n  return (\n    <div\n      className={cn(divider && \"pt-8 border-t border-border\", className)}\n      aria-labelledby={HeadingTag ? headingId : undefined}\n    >\n      {HeadingTag ? (\n        <HeadingTag\n          id={headingId}\n          className={cn(\n            \"text-sm font-semibold text-muted-foreground mb-3\",\n            headerClassName\n          )}\n        >\n          {resolvedLabels.heading}\n        </HeadingTag>\n      ) : null}\n\n      <ul role=\"list\" className=\"flex flex-wrap items-center gap-2 list-none\">\n        {targets.map((target) => {\n          const key = target.kind === \"custom\" ? target.id : target.kind;\n          return (\n            <ShareButton\n              key={key}\n              target={target}\n              state={target.kind === \"copy\" ? copyState : \"idle\"}\n              onClick={handleClick(target)}\n              buttonClassName={buttonClassName}\n              copyAriaLabel={resolvedLabels.copyAria}\n            />\n          );\n        })}\n      </ul>\n\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {copyState === \"success\" ? resolvedLabels.copySuccess : \"\"}\n      </span>\n      <span className=\"sr-only\" role=\"alert\">\n        {copyState === \"error\" ? resolvedLabels.copyError : \"\"}\n      </span>\n    </div>\n  );\n}\n\nexport const ShareBar = memo(ShareBarInner);\nShareBar.displayName = \"ShareBar\";\n",
      "type": "registry:component",
      "target": "components/share-bar/share-bar.tsx"
    },
    {
      "path": "src/registry/components/marketing/share-bar/index.ts",
      "content": "export { ShareBar } from \"./share-bar\";\nexport {\n  SHARE_BAR_DEFAULT_LABELS,\n  type ShareBarLabels,\n  type ShareBarProps,\n  type ShareKind,\n  type ShareTarget,\n  type ShareTargetBuiltin,\n  type ShareTargetCopy,\n  type ShareTargetCustom,\n} from \"./types\";\nexport { SHARE_TEMPLATES } from \"./parts/templates\";\n",
      "type": "registry:component",
      "target": "components/share-bar/index.ts"
    },
    {
      "path": "src/registry/components/marketing/share-bar/types.ts",
      "content": "import type { ComponentType } from \"react\";\n\nexport type ShareKind =\n  | \"twitter\"\n  | \"facebook\"\n  | \"linkedin\"\n  | \"reddit\"\n  | \"whatsapp\"\n  | \"telegram\"\n  | \"email\"\n  | \"threads\"\n  | \"bluesky\";\n\nexport interface ShareTargetBuiltin {\n  kind: ShareKind;\n  icon?: ComponentType<{ className?: string }>;\n  ariaLabel?: string;\n}\n\nexport interface ShareTargetCopy {\n  kind: \"copy\";\n  icon?: ComponentType<{ className?: string }>;\n  ariaLabel?: string;\n}\n\nexport interface ShareTargetCustom {\n  kind: \"custom\";\n  id: string;\n  icon: ComponentType<{ className?: string }>;\n  ariaLabel: string;\n  onClick: () => void;\n}\n\nexport type ShareTarget = ShareTargetBuiltin | ShareTargetCopy | ShareTargetCustom;\n\nexport interface ShareBarLabels {\n  heading?: string;\n  copyAria?: string;\n  copySuccess?: string;\n  copyError?: string;\n}\n\nexport interface ShareBarProps {\n  targets: ReadonlyArray<ShareTarget>;\n  url?: string;\n  title?: string;\n  text?: string;\n  via?: string;\n  hashtags?: ReadonlyArray<string>;\n  onShare?: (targetKind: string) => void;\n  onCopySuccess?: () => void;\n  onCopyError?: (err: unknown) => void;\n  successResetMs?: number;\n  divider?: boolean;\n  headingAs?: \"h2\" | \"h3\" | \"h4\" | null;\n  labels?: ShareBarLabels;\n  className?: string;\n  headerClassName?: string;\n  buttonClassName?: string;\n}\n\nexport const SHARE_BAR_DEFAULT_LABELS: Required<ShareBarLabels> = {\n  heading: \"Share\",\n  copyAria: \"Copy link\",\n  copySuccess: \"Link copied\",\n  copyError: \"Couldn't copy link\",\n};\n",
      "type": "registry:component",
      "target": "components/share-bar/types.ts"
    },
    {
      "path": "src/registry/components/marketing/share-bar/parts/share-button.tsx",
      "content": "import { Check, X } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { DEFAULT_ARIA, DEFAULT_ICONS } from \"./icons\";\nimport type { ShareTarget } from \"../types\";\n\nexport type CopyState = \"idle\" | \"success\" | \"error\";\n\ninterface ShareButtonProps {\n  target: ShareTarget;\n  state: CopyState;\n  onClick: () => void;\n  buttonClassName?: string;\n  copyAriaLabel?: string;\n}\n\nexport function ShareButton({\n  target,\n  state,\n  onClick,\n  buttonClassName,\n  copyAriaLabel,\n}: ShareButtonProps) {\n  const isCopy = target.kind === \"copy\";\n  const isCustom = target.kind === \"custom\";\n\n  const Icon =\n    isCopy && state === \"success\"\n      ? Check\n      : isCopy && state === \"error\"\n        ? X\n        : target.icon ??\n          (isCustom ? null : DEFAULT_ICONS[target.kind]);\n\n  const ariaLabel = isCustom\n    ? target.ariaLabel\n    : isCopy\n      ? target.ariaLabel ?? copyAriaLabel ?? DEFAULT_ARIA.copy\n      : target.ariaLabel ?? DEFAULT_ARIA[target.kind];\n\n  return (\n    <li>\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        size=\"icon\"\n        className={cn(\n          \"rounded-full\",\n          isCopy && state === \"success\" && \"text-primary\",\n          isCopy && state === \"error\" && \"text-destructive\",\n          buttonClassName\n        )}\n        title={ariaLabel}\n        aria-label={ariaLabel}\n        onClick={onClick}\n      >\n        {Icon ? <Icon className=\"w-4 h-4\" /> : null}\n      </Button>\n    </li>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/share-bar/parts/share-button.tsx"
    },
    {
      "path": "src/registry/components/marketing/share-bar/parts/templates.ts",
      "content": "import type { ShareKind } from \"../types\";\n\nexport interface ShareUrlContext {\n  url: string;\n  title?: string;\n  text?: string;\n  via?: string;\n  hashtags?: ReadonlyArray<string>;\n}\n\ntype Builder = (ctx: ShareUrlContext) => string;\n\nconst enc = encodeURIComponent;\n\nexport const SHARE_TEMPLATES: Record<ShareKind, Builder> = {\n  twitter: ({ url, title, via, hashtags }) => {\n    const params = new URLSearchParams({ url });\n    if (title) params.set(\"text\", title);\n    if (via) params.set(\"via\", via);\n    if (hashtags && hashtags.length > 0) {\n      params.set(\"hashtags\", hashtags.join(\",\"));\n    }\n    return `https://twitter.com/intent/tweet?${params.toString()}`;\n  },\n  facebook: ({ url }) =>\n    `https://www.facebook.com/sharer/sharer.php?u=${enc(url)}`,\n  linkedin: ({ url }) =>\n    `https://www.linkedin.com/sharing/share-offsite/?url=${enc(url)}`,\n  reddit: ({ url, title }) => {\n    const params = new URLSearchParams({ url });\n    if (title) params.set(\"title\", title);\n    return `https://reddit.com/submit?${params.toString()}`;\n  },\n  whatsapp: ({ url, title }) => {\n    const text = [title, url].filter(Boolean).join(\" \");\n    return `https://wa.me/?text=${enc(text)}`;\n  },\n  telegram: ({ url, title }) => {\n    const params = new URLSearchParams({ url });\n    if (title) params.set(\"text\", title);\n    return `https://t.me/share/url?${params.toString()}`;\n  },\n  email: ({ url, title, text }) => {\n    // v0.1.1 — mailto: is NOT an HTTP form target. URLSearchParams encodes\n    // spaces as `+`, which mail clients render literally (\"Check+this+out\").\n    // RFC 6068 wants percent-encoding → encodeURIComponent.\n    const query: string[] = [];\n    if (title) query.push(`subject=${enc(title)}`);\n    query.push(`body=${enc([text, url].filter(Boolean).join(\"\\n\\n\"))}`);\n    return `mailto:?${query.join(\"&\")}`;\n  },\n  threads: ({ url, title }) => {\n    const text = [title, url].filter(Boolean).join(\" \");\n    return `https://www.threads.net/intent/post?text=${enc(text)}`;\n  },\n  bluesky: ({ url, title }) => {\n    const text = [title, url].filter(Boolean).join(\" \");\n    return `https://bsky.app/intent/compose?text=${enc(text)}`;\n  },\n};\n",
      "type": "registry:component",
      "target": "components/share-bar/parts/templates.ts"
    },
    {
      "path": "src/registry/components/marketing/share-bar/parts/icons.ts",
      "content": "import { Copy, Mail, MessageCircle, Send } from \"lucide-react\";\nimport type { ComponentType } from \"react\";\nimport {\n  Bluesky,\n  Facebook,\n  LinkedIn,\n  Reddit,\n  Threads,\n  TwitterX,\n} from \"./brand-icons\";\nimport type { ShareKind } from \"../types\";\n\nexport const DEFAULT_ICONS: Record<\n  ShareKind | \"copy\",\n  ComponentType<{ className?: string }>\n> = {\n  twitter: TwitterX,\n  facebook: Facebook,\n  linkedin: LinkedIn,\n  reddit: Reddit,\n  whatsapp: MessageCircle,\n  telegram: Send,\n  email: Mail,\n  threads: Threads,\n  bluesky: Bluesky,\n  copy: Copy,\n};\n\nexport const DEFAULT_ARIA: Record<ShareKind | \"copy\", string> = {\n  twitter: \"Share on X\",\n  facebook: \"Share on Facebook\",\n  linkedin: \"Share on LinkedIn\",\n  reddit: \"Share on Reddit\",\n  whatsapp: \"Share on WhatsApp\",\n  telegram: \"Share on Telegram\",\n  email: \"Share via email\",\n  threads: \"Share on Threads\",\n  bluesky: \"Share on Bluesky\",\n  copy: \"Copy link\",\n};\n",
      "type": "registry:component",
      "target": "components/share-bar/parts/icons.ts"
    },
    {
      "path": "src/registry/components/marketing/share-bar/parts/brand-icons.tsx",
      "content": "import type { ComponentType, SVGProps } from \"react\";\n\ninterface BrandIconProps {\n  className?: string;\n}\n\nfunction makeBrand(path: string): ComponentType<BrandIconProps> {\n  function Icon({ className }: BrandIconProps) {\n    const svgProps: SVGProps<SVGSVGElement> = {\n      xmlns: \"http://www.w3.org/2000/svg\",\n      viewBox: \"0 0 24 24\",\n      fill: \"currentColor\",\n      \"aria-hidden\": true,\n      className,\n    };\n    return (\n      <svg {...svgProps}>\n        <path d={path} />\n      </svg>\n    );\n  }\n  Icon.displayName = \"BrandIcon\";\n  return Icon;\n}\n\n// Brand SVG paths (simple-icons.org public domain set, light fidelity).\n// Inlined here to avoid pulling react-icons (~250KB) for 6 brand glyphs.\n\nexport const TwitterX = makeBrand(\n  \"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z\"\n);\n\nexport const Facebook = makeBrand(\n  \"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z\"\n);\n\nexport const LinkedIn = makeBrand(\n  \"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.063 2.063 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\"\n);\n\nexport const Reddit = makeBrand(\n  \"M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.04 1.605a3.32 3.32 0 0 1 .053.582c0 2.971-3.36 5.382-7.494 5.382s-7.494-2.411-7.494-5.382c0-.198.019-.394.055-.583-.602-.273-1.034-.890-1.034-1.604 0-.968.786-1.754 1.754-1.754.477 0 .898.182 1.207.49 1.207-.864 2.878-1.422 4.744-1.486l.896-4.21a.32.32 0 0 1 .397-.247l2.93.62a1.249 1.249 0 0 1 1.235-1.099zm-7.81 6.762a1.252 1.252 0 0 0-1.246 1.252 1.252 1.252 0 0 0 2.504.005c0-.692-.56-1.252-1.249-1.257zm5.5 0a1.252 1.252 0 0 0-1.249 1.252 1.252 1.252 0 0 0 2.5.005c0-.692-.561-1.252-1.252-1.257zm-5.466 3.358a.328.328 0 0 0-.235.567c.91.910 2.65.978 3.158.978.51 0 2.25-.067 3.16-.978a.328.328 0 0 0 0-.466.328.328 0 0 0-.466 0c-.575.575-1.806.78-2.69.78-.883 0-2.115-.205-2.69-.78a.328.328 0 0 0-.236-.10z\"\n);\n\nexport const Threads = makeBrand(\n  \"M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.781 3.631 2.695 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.066-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 8.85c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.32.143 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65zm1.003-11.06c-.232 0-.467.007-.707.022-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.247-.222z\"\n);\n\nexport const Bluesky = makeBrand(\n  \"M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z\"\n);\n",
      "type": "registry:component",
      "target": "components/share-bar/parts/brand-icons.tsx"
    }
  ],
  "categories": [
    "marketing"
  ],
  "type": "registry:block"
}