{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-menu",
  "title": "Share Menu",
  "description": "A share menu with native sharing, copy-link feedback, and channel links.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/share-menu.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Check, Link2, Share, Share2 } from \"lucide-react\";\nimport { type ComponentProps, createContext, useContext, useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const SHARE_MENU_VARIANTS = [\"outlined\", \"ghost\", \"compact\"] as const;\nexport type ShareMenuVariant = (typeof SHARE_MENU_VARIANTS)[number];\nexport type ShareMenuProps = ComponentProps<\"div\"> & {\n  variant?: ShareMenuVariant;\n  /** The link to share and copy. */\n  url: string;\n  /** Title passed to the native share sheet. */\n  shareTitle?: string;\n  /** Text passed to the native share sheet. */\n  shareText?: string;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\ntype ShareContext = {\n  variant: ShareMenuVariant;\n  url: string;\n  shareTitle?: string;\n  shareText?: string;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  focusTarget: React.RefObject<\"first\" | \"last\">;\n  copied: boolean;\n  announce: (message: string, copied?: boolean) => void;\n};\nconst Context = createContext<ShareContext | null>(null);\nfunction useShare(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within ShareMenu`);\n  return context;\n}\nconst shareMenuTriggerVariants = cva(\n  \"gap-1.5 rounded-full border-0 py-0 font-medium whitespace-nowrap transition-[background-color,box-shadow,color,scale] duration-[120ms,120ms,120ms,140ms] ease-[ease-out,ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring active:scale-[0.97] data-copied:[&_svg]:animate-in data-copied:[&_svg]:fade-in-0 data-copied:[&_svg]:zoom-in-60 data-copied:[&_svg]:text-success-ink data-copied:[&_svg]:duration-220 data-copied:[&_svg]:ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none motion-reduce:active:scale-100 motion-reduce:data-copied:[&_svg]:animate-none\",\n  {\n    variants: {\n      variant: {\n        outlined:\n          \"h-7 bg-card pr-3 pl-2.5 text-[12.5px] text-foreground shadow-[inset_0_0_0_1px_var(--border)] hover:bg-accent hover:text-foreground hover:shadow-[inset_0_0_0_1px_var(--border-strong)] has-[>svg]:pr-3 has-[>svg]:pl-2.5 aria-expanded:bg-accent aria-expanded:shadow-[inset_0_0_0_1px_var(--border-strong)] dark:bg-card dark:hover:bg-accent dark:aria-expanded:bg-accent\",\n        ghost:\n          \"h-7 bg-transparent pr-3 pl-2.5 text-[12.5px] text-muted-foreground shadow-none hover:bg-accent hover:text-foreground has-[>svg]:pr-3 has-[>svg]:pl-2.5 aria-expanded:bg-accent aria-expanded:text-foreground dark:hover:bg-accent\",\n        compact:\n          \"h-6 bg-secondary pr-2.5 pl-2 text-[12px] text-foreground shadow-none hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] has-[>svg]:pr-2.5 has-[>svg]:pl-2 aria-expanded:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))]\",\n      },\n    },\n  },\n);\nconst shareMenuTriggerButtonVariant = {\n  outlined: \"outline\",\n  ghost: \"ghost\",\n  compact: \"secondary\",\n} as const satisfies Record<ShareMenuVariant, ComponentProps<typeof Button>[\"variant\"]>;\nconst shareMenuItemVariants = cva(\n  \"box-border flex w-full cursor-pointer items-center rounded-[10px] border-0 bg-transparent py-0 text-start text-foreground no-underline outline-offset-[-2px] transition-[background-color] duration-120 ease-[ease-out] focus:bg-accent focus:text-foreground focus:outline-none [&_svg]:flex-none [&_svg]:text-muted-foreground [&_svg]:transition-[color] [&_svg]:duration-120 [&_svg]:ease-[ease-out] focus:[&_svg]:text-foreground motion-reduce:transition-none motion-reduce:[&_svg]:transition-none [&_svg:not([class*='size-'])]:size-auto\",\n  {\n    variants: {\n      variant: {\n        outlined: \"min-h-8 gap-2.5 px-2.5 text-[13px]/[18px]\",\n        ghost: \"min-h-8 gap-2.5 px-2.5 text-[13px]/[18px]\",\n        compact: \"min-h-7 gap-2 px-2 text-[12.5px]/[18px]\",\n      },\n    },\n  },\n);\n\nexport function ShareMenu({\n  variant = \"outlined\",\n  url,\n  shareTitle,\n  shareText,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  children,\n  ...props\n}: ShareMenuProps) {\n  const focusTarget = useRef<\"first\" | \"last\">(\"first\");\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [message, setMessage] = useState(\"\");\n  const [copied, setCopied] = useState(false);\n  const current = open ?? internalOpen;\n  const setOpen = (next: boolean) => {\n    if (open === undefined) setInternalOpen(next);\n    onOpenChange?.(next);\n  };\n  useEffect(() => {\n    if (!copied) return;\n    const timer = window.setTimeout(() => setCopied(false), 2000);\n    return () => window.clearTimeout(timer);\n  }, [copied]);\n  return (\n    <Context.Provider\n      value={{\n        variant,\n        url,\n        shareTitle,\n        shareText,\n        open: current,\n        setOpen,\n        focusTarget,\n        copied,\n        announce: (next, didCopy = false) => {\n          setMessage(next);\n          setCopied(didCopy);\n        },\n      }}\n    >\n      <DropdownMenu open={current} onOpenChange={setOpen} modal={false}>\n        <div\n          data-slot=\"share-menu\"\n          data-variant={variant}\n          className={cn(\"relative inline-flex text-[13px]/[18px] text-foreground\", className)}\n          {...props}\n        >\n          {children}\n          <span role=\"status\" className=\"sr-only\">\n            {message}\n          </span>\n        </div>\n      </DropdownMenu>\n    </Context.Provider>\n  );\n}\n\nexport function ShareMenuTrigger({\n  children = \"Share\",\n  onKeyDown,\n  className,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useShare(\"ShareMenuTrigger\");\n  const compact = context.variant === \"compact\";\n  const Icon = context.copied ? Check : Share2;\n  return (\n    <DropdownMenuTrigger\n      asChild\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        if (event.defaultPrevented) return;\n        context.focusTarget.current = event.key === \"ArrowUp\" ? \"last\" : \"first\";\n        if (event.key === \"ArrowUp\") {\n          event.preventDefault();\n          context.setOpen(true);\n        }\n      }}\n    >\n      <Button\n        variant={shareMenuTriggerButtonVariant[context.variant]}\n        data-slot=\"share-menu-trigger\"\n        className={cn(shareMenuTriggerVariants({ variant: context.variant }), className)}\n        {...props}\n        type=\"button\"\n        data-copied={context.copied ? \"\" : undefined}\n      >\n        <Icon\n          key={context.copied ? \"copied\" : \"share\"}\n          className={compact ? \"size-[13px]\" : \"size-3.5\"}\n          strokeWidth={1.75}\n          aria-hidden=\"true\"\n        />\n        {children}\n      </Button>\n    </DropdownMenuTrigger>\n  );\n}\n\nexport function ShareMenuContent({\n  align = \"start\",\n  \"aria-label\": label = \"Share options\",\n  onFocus,\n  onKeyDown,\n  className,\n  ...props\n}: Omit<ComponentProps<typeof DropdownMenuContent>, \"align\"> & { align?: \"start\" | \"end\" }) {\n  const context = useShare(\"ShareMenuContent\");\n  const { focusTarget } = context;\n  return (\n    <DropdownMenuContent\n      aria-label={label}\n      align={align}\n      sideOffset={6}\n      data-slot=\"share-menu-content\"\n      data-align={align}\n      className={cn(\n        \"z-20 grid gap-px rounded-[14px] border-0 bg-popover p-1 shadow-[0_0_0_1px_var(--border-strong),0_12px_28px_-10px_oklch(0_0_0/0.32),0_2px_6px_-2px_oklch(0_0_0/0.12)]\",\n        \"duration-180 ease-[cubic-bezier(0.16,1,0.3,1)] data-[state=closed]:zoom-out-96 data-closed:zoom-out-96 data-[state=open]:zoom-in-96 data-open:zoom-in-96 data-[state=open]:[--tw-enter-translate-x:0]! data-open:[--tw-enter-translate-x:0]! data-[state=open]:[--tw-enter-translate-y:0]! data-open:[--tw-enter-translate-y:0]! motion-reduce:data-[state=closed]:animate-none motion-reduce:data-closed:animate-none motion-reduce:data-[state=open]:animate-none motion-reduce:data-open:animate-none\",\n        context.variant === \"compact\" ? \"min-w-[184px]\" : \"min-w-[200px]\",\n        className,\n      )}\n      {...props}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        // Up/Down past either end wrap, because Radix menus only loop with a Radix-only prop\n        // (Base UI menus loop on their own). The menu's own item handler has already marked the\n        // event handled by now, so defaultPrevented can't gate this.\n        const step = { ArrowDown: 1, ArrowUp: -1 }[event.key];\n        if (step === undefined) return;\n        const items = Array.from(\n          event.currentTarget.querySelectorAll<HTMLElement>(\n            '[role=\"menuitem\"]:not([data-disabled])',\n          ),\n        );\n        const next = items.indexOf(document.activeElement as HTMLElement) + step;\n        if (next >= 0 && next < items.length) return;\n        event.preventDefault();\n        items[(next + items.length) % items.length]?.focus();\n      }}\n      onFocus={(event) => {\n        onFocus?.(event);\n        if (event.defaultPrevented || event.target !== event.currentTarget) return;\n        // ArrowUp on the trigger opens the menu on its last item.\n        const target = focusTarget.current;\n        focusTarget.current = \"first\";\n        if (target !== \"last\") return;\n        event.preventDefault();\n        const items = event.currentTarget.querySelectorAll<HTMLElement>(\n          '[role=\"menuitem\"]:not([data-disabled])',\n        );\n        items[items.length - 1]?.focus();\n      }}\n    />\n  );\n}\n\n/** Opens the operating system share sheet. Renders only where `navigator.share` exists. */\nexport function ShareMenuNative({\n  children = \"Share via…\",\n  onClick,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuItem>) {\n  const context = useShare(\"ShareMenuNative\");\n  const [supported, setSupported] = useState(false);\n  useEffect(() => setSupported(typeof navigator.share === \"function\"), []);\n  if (!supported) return null;\n  return (\n    <DropdownMenuItem\n      data-slot=\"share-menu-native\"\n      className={cn(shareMenuItemVariants({ variant: context.variant }), className)}\n      {...props}\n      onClick={async (event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        try {\n          await navigator.share({\n            url: context.url,\n            title: context.shareTitle,\n            text: context.shareText,\n          });\n        } catch (error) {\n          if ((error as Error)?.name !== \"AbortError\") context.announce(\"Sharing failed\");\n        }\n      }}\n    >\n      <Share className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n      {children}\n    </DropdownMenuItem>\n  );\n}\n\nexport function ShareMenuCopy({\n  children = \"Copy link\",\n  copiedMessage = \"Link copied to clipboard\",\n  errorMessage = \"Couldn’t copy the link\",\n  onClick,\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuItem> & { copiedMessage?: string; errorMessage?: string }) {\n  const context = useShare(\"ShareMenuCopy\");\n  return (\n    <DropdownMenuItem\n      data-slot=\"share-menu-copy\"\n      className={cn(shareMenuItemVariants({ variant: context.variant }), className)}\n      {...props}\n      onClick={async (event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        try {\n          await navigator.clipboard.writeText(context.url);\n          context.announce(copiedMessage, true);\n        } catch {\n          context.announce(errorMessage);\n        }\n      }}\n    >\n      <Link2 className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />\n      {children}\n    </DropdownMenuItem>\n  );\n}\n\n/** A link to a share destination. The menu closes after the link is followed. */\nexport function ShareMenuChannel({ className, ...props }: ComponentProps<\"a\">) {\n  const context = useShare(\"ShareMenuChannel\");\n  return (\n    <DropdownMenuItem\n      asChild\n      className={cn(shareMenuItemVariants({ variant: context.variant }), className)}\n    >\n      <a target=\"_blank\" rel=\"noopener noreferrer\" data-slot=\"share-menu-channel\" {...props} />\n    </DropdownMenuItem>\n  );\n}\n\nexport function ShareMenuSeparator({\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuSeparator>) {\n  useShare(\"ShareMenuSeparator\");\n  return (\n    <DropdownMenuSeparator\n      data-slot=\"share-menu-separator\"\n      className={cn(\"mx-2 my-1 h-px border-0 bg-border\", className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/share-menu.tsx"
    }
  ],
  "categories": [
    "content",
    "community"
  ],
  "type": "registry:ui"
}