Fortura/apps/www/public/r/copy-button.json
2025-08-20 04:12:49 -06:00

20 lines
4.3 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "copy-button",
"type": "registry:ui",
"title": "Copy Button",
"description": "A button with a copy to clipboard animation.",
"dependencies": [
"motion",
"lucide-react",
"class-variance-authority"
],
"files": [
{
"path": "registry/buttons/copy/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport { AnimatePresence, HTMLMotionProps, motion } from 'motion/react';\nimport { CheckIcon, CopyIcon } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n 'inline-flex items-center justify-center cursor-pointer rounded-md transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',\n {\n variants: {\n variant: {\n default:\n 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',\n muted: 'bg-muted text-muted-foreground',\n destructive:\n 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',\n outline:\n 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n secondary:\n 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n ghost:\n 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n },\n size: {\n default: 'size-8 rounded-lg [&_svg]:size-4',\n sm: 'size-6 [&_svg]:size-3',\n md: 'size-10 rounded-lg [&_svg]:size-5',\n lg: 'size-12 rounded-xl [&_svg]:size-6',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\ntype CopyButtonProps = Omit<HTMLMotionProps<'button'>, 'children' | 'onCopy'> &\n VariantProps<typeof buttonVariants> & {\n content?: string;\n delay?: number;\n onCopy?: (content: string) => void;\n isCopied?: boolean;\n onCopyChange?: (isCopied: boolean) => void;\n };\n\nfunction CopyButton({\n content,\n className,\n size,\n variant,\n delay = 3000,\n onClick,\n onCopy,\n isCopied,\n onCopyChange,\n ...props\n}: CopyButtonProps) {\n const [localIsCopied, setLocalIsCopied] = React.useState(isCopied ?? false);\n const Icon = localIsCopied ? CheckIcon : CopyIcon;\n\n React.useEffect(() => {\n setLocalIsCopied(isCopied ?? false);\n }, [isCopied]);\n\n const handleIsCopied = React.useCallback(\n (isCopied: boolean) => {\n setLocalIsCopied(isCopied);\n onCopyChange?.(isCopied);\n },\n [onCopyChange],\n );\n\n const handleCopy = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (isCopied) return;\n if (content) {\n navigator.clipboard\n .writeText(content)\n .then(() => {\n handleIsCopied(true);\n setTimeout(() => handleIsCopied(false), delay);\n onCopy?.(content);\n })\n .catch((error) => {\n console.error('Error copying command', error);\n });\n }\n onClick?.(e);\n },\n [isCopied, content, delay, onClick, onCopy, handleIsCopied],\n );\n\n return (\n <motion.button\n data-slot=\"copy-button\"\n whileHover={{ scale: 1.05 }}\n whileTap={{ scale: 0.95 }}\n className={cn(buttonVariants({ variant, size }), className)}\n onClick={handleCopy}\n {...props}\n >\n <AnimatePresence mode=\"wait\">\n <motion.span\n key={localIsCopied ? 'check' : 'copy'}\n data-slot=\"copy-button-icon\"\n initial={{ scale: 0 }}\n animate={{ scale: 1 }}\n exit={{ scale: 0 }}\n transition={{ duration: 0.15 }}\n >\n <Icon />\n </motion.span>\n </AnimatePresence>\n </motion.button>\n );\n}\n\nexport { CopyButton, buttonVariants, type CopyButtonProps };\n",
"type": "registry:ui",
"target": "components/animate-ui/buttons/copy.tsx"
}
]
}