{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uploadthing-button",
  "title": "UploadThing Button",
  "description": "A simple upload button with loading states and file validation. Self-contained component that works with any upload backend.",
  "dependencies": [
    "@uploadthing/react",
    "uploadthing"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/uploadthing/uploadthing-button/components/elements/uploadthing-button.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface UploadThingButtonProps {\n  onUpload?: (files: File[]) => Promise<void>;\n  onSelect?: (files: File[]) => void;\n  accept?: string;\n  multiple?: boolean;\n  maxFiles?: number;\n  maxSize?: number;\n  disabled?: boolean;\n  className?: string;\n  children?: React.ReactNode;\n}\n\nfunction UploadIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n      <polyline points=\"17 8 12 3 7 8\" />\n      <line x1=\"12\" x2=\"12\" y1=\"3\" y2=\"15\" />\n    </svg>\n  );\n}\n\nfunction LoadingSpinner({ className }: { className?: string }) {\n  return (\n    <svg\n      className={cn(\"animate-spin\", className)}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n    >\n      <circle\n        className=\"opacity-25\"\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke=\"currentColor\"\n        strokeWidth=\"4\"\n      />\n      <path\n        className=\"opacity-75\"\n        fill=\"currentColor\"\n        d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n      />\n    </svg>\n  );\n}\n\nfunction CheckIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <polyline points=\"20 6 9 17 4 12\" />\n    </svg>\n  );\n}\n\nexport function UploadThingButton({\n  onUpload,\n  onSelect,\n  accept = \"image/*\",\n  multiple = false,\n  maxFiles = 1,\n  maxSize = 4 * 1024 * 1024,\n  disabled = false,\n  className,\n  children,\n}: UploadThingButtonProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [isUploading, setIsUploading] = useState(false);\n  const [uploadComplete, setUploadComplete] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n\n  const handleClick = () => {\n    if (!disabled && !isUploading) {\n      inputRef.current?.click();\n    }\n  };\n\n  const handleFileChange = useCallback(\n    async (e: React.ChangeEvent<HTMLInputElement>) => {\n      const fileList = e.target.files;\n      if (!fileList || fileList.length === 0) return;\n\n      const files = Array.from(fileList).slice(0, maxFiles);\n      setError(null);\n      setUploadComplete(false);\n\n      for (const file of files) {\n        if (file.size > maxSize) {\n          setError(`File too large. Max size: ${Math.round(maxSize / 1024 / 1024)}MB`);\n          e.target.value = \"\";\n          return;\n        }\n      }\n\n      onSelect?.(files);\n\n      if (onUpload) {\n        try {\n          setIsUploading(true);\n          await onUpload(files);\n          setUploadComplete(true);\n          setTimeout(() => setUploadComplete(false), 2000);\n        } catch (err) {\n          setError(err instanceof Error ? err.message : \"Upload failed\");\n        } finally {\n          setIsUploading(false);\n        }\n      }\n\n      e.target.value = \"\";\n    },\n    [maxFiles, maxSize, onSelect, onUpload]\n  );\n\n  return (\n    <div data-slot=\"uploadthing-button\" className={cn(\"inline-flex flex-col items-start gap-1\", className)}>\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        disabled={disabled || isUploading}\n        className={cn(\n          \"inline-flex items-center justify-center gap-2 px-4 py-2 rounded-md\",\n          \"bg-primary text-primary-foreground font-medium text-sm\",\n          \"transition-all duration-200\",\n          \"hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n          \"disabled:opacity-50 disabled:pointer-events-none\",\n          uploadComplete && \"bg-green-600 hover:bg-green-600\"\n        )}\n      >\n        {isUploading ? (\n          <>\n            <LoadingSpinner className=\"w-4 h-4\" />\n            <span>Uploading...</span>\n          </>\n        ) : uploadComplete ? (\n          <>\n            <CheckIcon className=\"w-4 h-4\" />\n            <span>Uploaded!</span>\n          </>\n        ) : (\n          <>\n            <UploadIcon className=\"w-4 h-4\" />\n            <span>{children || \"Choose File\"}</span>\n          </>\n        )}\n      </button>\n\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept={accept}\n        multiple={multiple}\n        onChange={handleFileChange}\n        disabled={disabled || isUploading}\n        className=\"sr-only\"\n        aria-label=\"Upload file\"\n      />\n\n      {error && (\n        <p className=\"text-xs text-destructive\">{error}</p>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Self-contained upload button with progress indication, file validation, and customizable accept types. Wire up to any upload backend via the onUpload prop.",
  "categories": [
    "uploadthing"
  ],
  "type": "registry:block"
}