{
  "name": "use-clipboard",
  "author": "Brendan Dash (https://shadcn-hooks.com)",
  "description": "A hook to copy text to clipboard",
  "registryDependencies": [
    "@shadcnhooks/use-unmount",
    "@shadcnhooks/use-event-listener",
    "@shadcnhooks/use-memoized-fn",
    "@shadcnhooks/is-browser"
  ],
  "files": [
    {
      "path": "registry/hooks/use-clipboard.ts",
      "content": "import { useEffect, useMemo, useRef, useState } from 'react'\nimport { useEventListener } from '@/registry/hooks/use-event-listener'\nimport { useMemoizedFn } from '@/registry/hooks/use-memoized-fn'\nimport { useUnmount } from '@/registry/hooks/use-unmount'\nimport { isBrowser } from '@/registry/lib/is-browser'\n\nexport interface UseClipboardOptions {\n  /**\n   * Enabled reading for clipboard\n   *\n   * @default false\n   */\n  read?: boolean\n\n  /**\n   * Copy source\n   */\n  source?: string\n\n  /**\n   * Milliseconds to reset state of `copied` ref\n   *\n   * @default 1500\n   */\n  copiedDuring?: number\n\n  /**\n   * Whether fallback to document.execCommand('copy') if clipboard is undefined.\n   *\n   * @default false\n   */\n  legacy?: boolean\n}\n\nexport interface UseClipboardReturn {\n  isSupported: boolean\n  text: string\n  copied: boolean\n  copy: (text?: string) => Promise<void>\n}\n\ntype PermissionState = 'granted' | 'denied' | 'prompt' | undefined\n\nfunction isAllowed(status: PermissionState): boolean {\n  return status === 'granted' || status === 'prompt'\n}\n\nfunction legacyCopy(value: string): void {\n  const ta = document.createElement('textarea')\n  ta.value = value\n  ta.style.position = 'absolute'\n  ta.style.opacity = '0'\n  ta.setAttribute('readonly', '')\n  document.body.appendChild(ta)\n  ta.select()\n  document.execCommand('copy')\n  ta.remove()\n}\n\nfunction legacyRead(): string {\n  return document?.getSelection?.()?.toString() ?? ''\n}\n\n/**\n * Reactive Clipboard API.\n *\n * @param options - Configuration options\n * @returns Clipboard state and methods\n */\nexport function useClipboard(\n  options: UseClipboardOptions = {},\n): UseClipboardReturn {\n  const { read = false, source, copiedDuring = 1500, legacy = false } = options\n\n  const [text, setText] = useState<string>('')\n  const [copied, setCopied] = useState<boolean>(false)\n  const [permissionRead, setPermissionRead] =\n    useState<PermissionState>(undefined)\n  const [permissionWrite, setPermissionWrite] =\n    useState<PermissionState>(undefined)\n\n  const timeoutRef = useRef<number | null>(null)\n\n  const isClipboardApiSupported = useMemo(() => {\n    if (!isBrowser) return false\n    return 'clipboard' in navigator\n  }, [])\n\n  const isSupported = useMemo(() => {\n    return isClipboardApiSupported || legacy\n  }, [isClipboardApiSupported, legacy])\n\n  // Check permissions\n  useEffect(() => {\n    if (!isBrowser || !isClipboardApiSupported) return\n\n    const checkPermissions = async () => {\n      try {\n        if ('permissions' in navigator) {\n          const readPermission = await navigator.permissions.query({\n            name: 'clipboard-read' as PermissionName,\n          })\n          setPermissionRead(readPermission.state)\n          readPermission.onchange = () => {\n            setPermissionRead(readPermission.state)\n          }\n\n          const writePermission = await navigator.permissions.query({\n            name: 'clipboard-write' as PermissionName,\n          })\n          setPermissionWrite(writePermission.state)\n          writePermission.onchange = () => {\n            setPermissionWrite(writePermission.state)\n          }\n        }\n      } catch {\n        // Permissions API might not be supported or clipboard permissions might not be queryable\n        // In this case, we'll try to use the clipboard API directly\n      }\n    }\n\n    checkPermissions()\n  }, [isClipboardApiSupported])\n\n  const updateText = useMemoizedFn(async () => {\n    let useLegacy = !(isClipboardApiSupported && isAllowed(permissionRead))\n    if (!useLegacy) {\n      try {\n        const clipboardText = await navigator.clipboard.readText()\n        setText(clipboardText)\n      } catch {\n        useLegacy = true\n      }\n    }\n    if (useLegacy) {\n      setText(legacyRead())\n    }\n  })\n\n  // Listen to copy/cut events if read is enabled\n  useEventListener(isSupported && read ? ['copy', 'cut'] : [], updateText, {\n    passive: true,\n    enable: isSupported && read,\n  })\n\n  const copy = useMemoizedFn(async (value?: string) => {\n    const textToCopy = value ?? source\n    if (!isSupported || textToCopy == null) return\n\n    let useLegacy = !(isClipboardApiSupported && isAllowed(permissionWrite))\n    if (!useLegacy) {\n      try {\n        await navigator.clipboard.writeText(textToCopy)\n      } catch {\n        useLegacy = true\n      }\n    }\n    if (useLegacy) {\n      legacyCopy(textToCopy)\n    }\n\n    setText(textToCopy)\n    setCopied(true)\n\n    // Clear existing timeout\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n    }\n\n    // Set new timeout\n    timeoutRef.current = window.setTimeout(() => {\n      setCopied(false)\n      timeoutRef.current = null\n    }, copiedDuring)\n  })\n\n  // Cleanup timeout on unmount\n  useUnmount(() => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n    }\n  })\n\n  return {\n    isSupported,\n    text,\n    copied,\n    copy,\n  }\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}
