{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-affordance",
  "title": "Copy Affordance",
  "description": "Lightweight copy-to-clipboard wrapper for read values. Field schema uses copyable; not a Data Display product page.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/components/f-ui/copy-affordance/copy-affordance.tsx",
      "content": "\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useClipboardCopy } from \"./use-clipboard-copy\";\n\nexport type CopyAffordanceClassNames = { root?: string; button?: string };\n\nexport type CopyAffordanceProps = {\n  text: string;\n  children: React.ReactNode;\n  visibility?: \"hover\" | \"always\"; // default \"hover\" — P4; \"always\" ≈ Ant prominence\n  showCopy?: boolean;\n  onCopy?: () => void;\n  copyLabel?: string; // Ant default \"Copy\"\n  copiedLabel?: string; // Ant default \"Copied\"\n  className?: string;\n  classNames?: CopyAffordanceClassNames;\n};\n\nexport function CopyAffordance({\n  text,\n  children,\n  visibility = \"hover\",\n  showCopy = true,\n  onCopy,\n  copyLabel = \"Copy\",\n  copiedLabel = \"Copied\",\n  className,\n  classNames,\n}: CopyAffordanceProps) {\n  const { copied, copy } = useClipboardCopy({\n    text,\n    onCopy: () => onCopy?.(),\n  });\n\n  const handleCopy = () => {\n    void copy();\n  };\n\n  return (\n    <span\n      data-slot=\"copy-affordance\"\n      className={cn(\n        \"group/copy inline-flex min-w-0 items-center gap-1\",\n        className,\n        classNames?.root,\n      )}\n    >\n      <span className=\"min-w-0\">{children}</span>\n      {showCopy ? (\n        <button\n          type=\"button\"\n          onClick={handleCopy}\n          aria-label={copied ? copiedLabel : copyLabel}\n          className={cn(\n            \"text-muted-foreground/60 hover:text-foreground inline-flex shrink-0 items-center rounded-md p-1 transition\",\n            \"focus-visible:outline-none focus-visible:opacity-100\",\n            visibility === \"hover\" &&\n              \"opacity-0 group-hover/copy:opacity-100 focus-visible:opacity-100\",\n            classNames?.button,\n          )}\n        >\n          {copied ? (\n            <Check className=\"size-3.5\" aria-hidden />\n          ) : (\n            <Copy className=\"size-3.5\" aria-hidden />\n          )}\n        </button>\n      ) : null}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/f-ui/copy-affordance/copy-affordance.tsx"
    },
    {
      "path": "src/components/f-ui/copy-affordance/format-truncated-text.ts",
      "content": "export const DEFAULT_TRUNCATED_ELLIPSIS = \"\\u2026\";\n\nexport type TruncatedTextMiddleOptions = {\n  mode: \"middle\";\n  /** Characters to keep at the start. @default 0 */\n  prefix?: number;\n  /** Characters to keep at the end. @default 0 */\n  suffix?: number;\n  /** Middle replacement when truncated. @default Unicode ellipsis (…) */\n  ellipsis?: string;\n};\n\nexport type TruncatedTextEndOptions = {\n  mode: \"end\";\n  /**\n   * When false, clip overflow without an ellipsis character (CSS `truncate`\n   * still applies). @default true\n   */\n  ellipsis?: boolean;\n};\n\nexport type TruncatedTextOptions =\n  | TruncatedTextMiddleOptions\n  | TruncatedTextEndOptions;\n\nexport function formatTruncatedText(\n  text: string,\n  truncate?: TruncatedTextOptions | false,\n): string {\n  if (!truncate || truncate.mode === \"end\") {\n    return text;\n  }\n\n  const prefix = Math.max(0, truncate.prefix ?? 0);\n  const suffix = Math.max(0, truncate.suffix ?? 0);\n  const ellipsis = truncate.ellipsis ?? DEFAULT_TRUNCATED_ELLIPSIS;\n\n  if (prefix === 0 && suffix === 0) {\n    return text;\n  }\n\n  const keepLength = prefix + suffix;\n  if (text.length <= keepLength) {\n    return text;\n  }\n\n  if (text.length <= keepLength + ellipsis.length) {\n    return text;\n  }\n\n  if (suffix === 0) {\n    return `${text.slice(0, prefix)}${ellipsis}`;\n  }\n  if (prefix === 0) {\n    return `${ellipsis}${text.slice(-suffix)}`;\n  }\n\n  return `${text.slice(0, prefix)}${ellipsis}${text.slice(-suffix)}`;\n}\n",
      "type": "registry:lib",
      "target": "components/f-ui/copy-affordance/format-truncated-text.ts"
    },
    {
      "path": "src/components/f-ui/copy-affordance/resolve-copy-text.ts",
      "content": "type CopyableConfig = {\n  text?: string | ((value: unknown) => string);\n};\n\ntype ResolveCopyTextConfig = {\n  copyable?: boolean | CopyableConfig;\n};\n\nfunction isCopyPrimitive(value: unknown): value is string | number | boolean {\n  return typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\";\n}\n\n/** Returns clipboard text, or null when copy must not attach (Pro genCopyable gate). */\nexport function resolveCopyText(\n  config: ResolveCopyTextConfig,\n  value: unknown,\n): string | null {\n  if (!config.copyable) return null;\n  if (value == null || value === \"\") return null;\n\n  const cfg = typeof config.copyable === \"object\" ? config.copyable : undefined;\n  if (cfg?.text !== undefined) {\n    return typeof cfg.text === \"function\" ? cfg.text(value) : cfg.text;\n  }\n  if (!isCopyPrimitive(value)) return null;\n  return String(value);\n}\n",
      "type": "registry:lib",
      "target": "components/f-ui/copy-affordance/resolve-copy-text.ts"
    },
    {
      "path": "src/components/f-ui/copy-affordance/use-clipboard-copy.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport type UseClipboardCopyOptions = {\n  text?: string\n  resetMs?: number\n  onCopy?: (text: string) => void\n  onError?: (error: unknown) => void\n}\n\nexport type UseClipboardCopyReturn = {\n  copied: boolean\n  copy: (overrideText?: string) => Promise<boolean>\n  reset: () => void\n}\n\nexport function useClipboardCopy(\n  options: UseClipboardCopyOptions = {},\n): UseClipboardCopyReturn {\n  const { text, resetMs = 2000, onCopy, onError } = options\n  const [copied, setCopied] = React.useState(false)\n  const timerRef = React.useRef<number | null>(null)\n\n  const clearTimer = React.useCallback(() => {\n    if (timerRef.current != null) {\n      window.clearTimeout(timerRef.current)\n      timerRef.current = null\n    }\n  }, [])\n\n  const reset = React.useCallback(() => {\n    clearTimer()\n    setCopied(false)\n  }, [clearTimer])\n\n  React.useEffect(() => () => clearTimer(), [clearTimer])\n\n  const copy = React.useCallback(\n    async (overrideText?: string) => {\n      const next = overrideText ?? text\n      if (next == null || next === \"\") return false\n      if (typeof navigator === \"undefined\" || !navigator.clipboard?.writeText) {\n        onError?.(new Error(\"clipboard unavailable\"))\n        return false\n      }\n      try {\n        await navigator.clipboard.writeText(next)\n        clearTimer()\n        setCopied(true)\n        onCopy?.(next)\n        timerRef.current = window.setTimeout(() => {\n          setCopied(false)\n          timerRef.current = null\n        }, resetMs)\n        return true\n      } catch (error) {\n        onError?.(error)\n        return false\n      }\n    },\n    [text, resetMs, onCopy, onError, clearTimer],\n  )\n\n  return { copied, copy, reset }\n}\n",
      "type": "registry:lib",
      "target": "components/f-ui/copy-affordance/use-clipboard-copy.ts"
    }
  ],
  "type": "registry:lib"
}