{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "clipboard",
  "title": "Clipboard",
  "description": "One-press copy with a clear result: a lone icon, a code line, a read-only field or a labelled block, and a safe way out when the browser refuses.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/clipboard.tsx",
      "content": "import type * as React from \"react\"\nimport { useClipboard, copyText, type ClipboardStatus } from \"@/registry/miseui/ui/clipboard-state\"\nimport { BlockBody, CopyButton, FieldBody, InlineBody } from \"@/registry/miseui/ui/clipboard-parts\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ClipboardVariant = \"icon\" | \"inline\" | \"field\" | \"block\"\ntype ClipboardEffect = \"morph\"\n\nconst ROOT_CLASS: Record<ClipboardVariant, string> = {\n  icon: \"inline-flex\",\n  inline: \"grid w-full min-w-0 gap-1\",\n  field: \"grid w-full min-w-0 gap-1\",\n  block: \"grid w-full min-w-0 gap-1\",\n}\nconst BUTTON_SHAPE = { icon: \"icon\", inline: \"icon\", field: \"joined\", block: \"header\" } as const\n\ntype ClipboardProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"title\"> & {\n  /** The exact text that lands on the clipboard. */\n  value: string\n  /** `icon` a lone button, `inline` a code line, `field` a read-only input with a button, `block` several lines under a heading. */\n  variant?: ClipboardVariant\n  /** `morph` turns the copy glyph into the check instead of swapping it. */\n  effect?: ClipboardEffect\n  /** The button name. */\n  label?: string\n  copiedLabel?: string\n  /** Shown when the browser refuses. The text is selected at that moment, so it should say to copy by hand. */\n  failedLabel?: string\n  /** Name of the text for assistive tech in `inline`, `field` and `block`. */\n  valueLabel?: string\n  /** Heading of `block`, usually a file name. */\n  title?: React.ReactNode\n  disabled?: boolean\n  /** How long the check stays, in milliseconds. */\n  resetAfter?: number\n  onCopied?: () => void\n  onFailed?: () => void\n}\n\nfunction Clipboard({\n  value,\n  variant = \"icon\",\n  effect,\n  label = \"Copy\",\n  copiedLabel = \"Copied\",\n  failedLabel = \"Could not copy. The text is selected, so copy it by hand.\",\n  valueLabel = \"Text to copy\",\n  title,\n  disabled,\n  resetAfter = 2000,\n  onCopied,\n  onFailed,\n  className,\n  ...props\n}: ClipboardProps) {\n  const { status, copy, textRef } = useClipboard({ value, resetAfter, onCopied, onFailed })\n  const button = (\n    <CopyButton\n      {...{ status, label, copiedLabel, disabled }}\n      shape={BUTTON_SHAPE[variant]}\n      morph={effect === \"morph\"}\n      onClick={copy}\n    />\n  )\n  const body = { value, valueLabel, textRef, button }\n  const message = status === \"copied\" ? copiedLabel : status === \"failed\" ? failedLabel : \"\"\n  const showFailure = status === \"failed\" && variant !== \"icon\"\n  return (\n    <div\n      data-slot=\"clipboard\"\n      data-variant={variant}\n      data-effect={effect}\n      data-state={status}\n      className={cn(ROOT_CLASS[variant], className)}\n      {...props}\n    >\n      {variant === \"icon\" ? button : null}\n      {variant === \"inline\" ? <InlineBody {...body} /> : null}\n      {variant === \"field\" ? <FieldBody {...body} disabled={disabled} /> : null}\n      {variant === \"block\" ? <BlockBody {...body} title={title} /> : null}\n      <span\n        role=\"status\"\n        data-slot=\"clipboard-status\"\n        className={showFailure ? \"text-[13px] leading-5 text-destructive\" : \"sr-only\"}\n      >\n        {message}\n      </span>\n    </div>\n  )\n}\n\nexport { Clipboard, copyText, type ClipboardProps, type ClipboardVariant, type ClipboardEffect, type ClipboardStatus }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/clipboard-parts.tsx",
      "content": "import type * as React from \"react\"\nimport { Check, Copy, X } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport type { ClipboardStatus } from \"@/registry/miseui/ui/clipboard-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst GLYPH: Record<ClipboardStatus, typeof Copy> = { idle: Copy, copied: Check, failed: X }\n/** Fixed hues: this is a result, and the accent may itself be amber or red. */\nconst GLYPH_TONE: Record<ClipboardStatus, string> = {\n  idle: \"\",\n  copied: \"text-emerald-600 dark:text-emerald-400\",\n  failed: \"text-destructive\",\n}\nconst BUTTON_BASE = [\n  \"inline-flex shrink-0 items-center justify-center gap-2 text-sm font-medium outline-none\",\n  \"transition-[color,background-color,box-shadow,scale] duration-(--duration-fast) ease-(--ease-mise)\",\n  \"focus-visible:ring-[3px] focus-visible:ring-ring/50 active:scale-[0.97] active:duration-100 motion-reduce:active:scale-100\",\n  \"disabled:pointer-events-none disabled:opacity-50\",\n]\nconst QUIET = \"rounded-md text-muted-foreground hover:bg-foreground/5 hover:text-foreground\"\nconst BUTTON_SHAPE = {\n  icon: cn(QUIET, \"size-8 pointer-coarse:size-10\"),\n  joined: \"rounded-r-md border-l px-3 hover:bg-accent\",\n  header: cn(QUIET, \"h-8 px-2 text-[13px] pointer-coarse:h-10\"),\n}\nconst FOCUS_INSIDE = \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\"\n\nfunction CopyGlyph({ status, morph }: { status: ClipboardStatus; morph: boolean }) {\n  const reduceMotion = useReducedMotion() ?? false\n  const Icon = GLYPH[status]\n  if (!morph || reduceMotion) return <Icon aria-hidden=\"true\" className={cn(\"size-4 shrink-0\", GLYPH_TONE[status])} />\n  const away = { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }\n  return (\n    <span aria-hidden=\"true\" data-slot=\"clipboard-morph\" className=\"grid size-4 shrink-0 *:col-start-1 *:row-start-1\">\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={status}\n          className=\"block\"\n          initial={away}\n          animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n          exit={away}\n          transition={SPRING_DEFAULT}\n        >\n          <Icon className={cn(\"size-4\", GLYPH_TONE[status])} />\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  )\n}\n\ninterface CopyButtonProps {\n  shape: keyof typeof BUTTON_SHAPE\n  status: ClipboardStatus\n  morph: boolean\n  label: string\n  copiedLabel: string\n  disabled?: boolean\n  onClick: () => void\n}\n\nfunction CopyButton({ shape, status, morph, label, copiedLabel, disabled, onClick }: CopyButtonProps) {\n  const copied = status === \"copied\"\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"clipboard-button\"\n      data-state={status}\n      // The visible word goes invisible while copied, which would leave the button without a name.\n      aria-label={label}\n      disabled={disabled}\n      onClick={onClick}\n      className={cn(BUTTON_BASE, BUTTON_SHAPE[shape])}\n    >\n      <CopyGlyph status={status} morph={morph} />\n      {shape === \"icon\" ? null : (\n        // Both words stay in the grid, so the button keeps one width and the row beside it does not twitch on every copy.\n        <span className=\"grid *:col-start-1 *:row-start-1\">\n          <span className={cn(copied && \"invisible\")}>{label}</span>\n          <span aria-hidden=\"true\" className={cn(!copied && \"invisible\")}>\n            {copiedLabel}\n          </span>\n        </span>\n      )}\n    </button>\n  )\n}\n\ninterface BodyProps {\n  value: string\n  valueLabel: string\n  textRef: React.RefObject<HTMLElement | null>\n  button: React.ReactNode\n}\n\nfunction InlineBody({ value, valueLabel, textRef, button }: BodyProps) {\n  return (\n    <div className=\"flex min-w-0 items-center gap-1 rounded-md bg-muted pr-1 pl-3\">\n      <code\n        ref={textRef}\n        tabIndex={0}\n        aria-label={valueLabel}\n        className={cn(\"min-w-0 flex-1 overflow-x-auto py-2 font-mono text-[13px] leading-5 whitespace-nowrap\", FOCUS_INSIDE)}\n      >\n        {value}\n      </code>\n      {button}\n    </div>\n  )\n}\n\nfunction FieldBody({ value, valueLabel, textRef, button, disabled }: BodyProps & { disabled?: boolean }) {\n  return (\n    <div className=\"flex min-w-0 rounded-md border border-input shadow-xs\">\n      <input\n        ref={textRef as React.Ref<HTMLInputElement>}\n        readOnly\n        value={value}\n        aria-label={valueLabel}\n        disabled={disabled}\n        onFocus={(event) => event.currentTarget.select()}\n        className={cn(\"h-9 min-w-0 flex-1 rounded-l-md bg-transparent px-3 font-mono text-base pointer-coarse:h-10 md:text-[13px]\", FOCUS_INSIDE)}\n      />\n      {button}\n    </div>\n  )\n}\n\nfunction BlockBody({ value, valueLabel, textRef, button, title }: BodyProps & { title?: React.ReactNode }) {\n  return (\n    <div className=\"min-w-0 rounded-lg border bg-card text-card-foreground\">\n      <div className=\"flex items-center justify-between gap-3 border-b py-1 pr-1 pl-3\">\n        <span className=\"min-w-0 truncate text-[13px] text-muted-foreground\">{title}</span>\n        {button}\n      </div>\n      <pre\n        ref={textRef as React.Ref<HTMLPreElement>}\n        tabIndex={0}\n        aria-label={valueLabel}\n        className={cn(\"max-h-64 overflow-auto p-3 font-mono text-[13px] leading-5\", FOCUS_INSIDE)}\n      >\n        {value}\n      </pre>\n    </div>\n  )\n}\n\nexport { CopyButton, InlineBody, FieldBody, BlockBody }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/clipboard-state.ts",
      "content": "import { useEffect, useRef, useState } from \"react\"\n\ntype ClipboardStatus = \"idle\" | \"copied\" | \"failed\"\n\n/** Failure stays up longer: the person now has a manual step to do and needs time to read about it. */\nconst FAILED_HOLD_FACTOR = 3\n\n/** Never rejects. The API is missing on plain http and refused without a user gesture or with permissions denied. */\nasync function copyText(text: string) {\n  try {\n    if (!navigator.clipboard?.writeText) return false\n    await navigator.clipboard.writeText(text)\n    return true\n  } catch {\n    return false\n  }\n}\n\n/** Leaves the text selected, so the person whose browser refused can still copy it by hand. */\nfunction selectText(node: HTMLElement | null) {\n  if (!node) return\n  if (node instanceof HTMLInputElement) node.select()\n  else window.getSelection()?.selectAllChildren(node)\n}\n\ninterface UseClipboardOptions {\n  value: string\n  resetAfter: number\n  onCopied?: () => void\n  onFailed?: () => void\n}\n\nfunction useClipboard({ value, resetAfter, onCopied, onFailed }: UseClipboardOptions) {\n  const [status, setStatus] = useState<ClipboardStatus>(\"idle\")\n  const timer = useRef<number | undefined>(undefined)\n  const textRef = useRef<HTMLElement>(null)\n\n  useEffect(() => () => window.clearTimeout(timer.current), [])\n\n  const copy = async () => {\n    const copied = await copyText(value)\n    window.clearTimeout(timer.current)\n    setStatus(copied ? \"copied\" : \"failed\")\n    const hold = copied ? resetAfter : resetAfter * FAILED_HOLD_FACTOR\n    timer.current = window.setTimeout(() => setStatus(\"idle\"), hold)\n    if (copied) {\n      onCopied?.()\n      return\n    }\n    selectText(textRef.current)\n    onFailed?.()\n  }\n\n  return { status, copy, textRef }\n}\n\nexport { copyText, selectText, useClipboard, type ClipboardStatus }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}