{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "uploadthing-image-grid",
  "title": "UploadThing Image Grid",
  "description": "A multi-image upload grid with preview thumbnails, remove buttons, and configurable columns.",
  "dependencies": [
    "@uploadthing/react",
    "uploadthing"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/uploadthing/uploadthing-image-grid/components/elements/uploadthing-image-grid.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ImageItem {\n  id: string;\n  url: string;\n  name?: string;\n}\n\ninterface UploadThingImageGridProps {\n  value?: ImageItem[];\n  onChange?: (images: ImageItem[]) => void;\n  onUpload?: (files: File[]) => Promise<ImageItem[]>;\n  onRemove?: (id: string) => void;\n  maxImages?: number;\n  columns?: 2 | 3 | 4;\n  className?: string;\n  disabled?: boolean;\n}\n\nfunction PlusIcon({ 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=\"M12 5v14M5 12h14\" />\n    </svg>\n  );\n}\n\nfunction XIcon({ 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=\"M18 6 6 18M6 6l12 12\" />\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\nconst GRID_COLS = {\n  2: \"grid-cols-2\",\n  3: \"grid-cols-3\",\n  4: \"grid-cols-4\",\n};\n\nexport type { ImageItem };\n\nexport function UploadThingImageGrid({\n  value = [],\n  onChange,\n  onUpload,\n  onRemove,\n  maxImages = 9,\n  columns = 3,\n  className,\n  disabled = false,\n}: UploadThingImageGridProps) {\n  const [isUploading, setIsUploading] = useState(false);\n  const [uploadingPreviews, setUploadingPreviews] = useState<string[]>([]);\n\n  const canAddMore = value.length < maxImages;\n\n  const handleFileChange = useCallback(\n    async (e: React.ChangeEvent<HTMLInputElement>) => {\n      const files = Array.from(e.target.files || []);\n      if (files.length === 0) return;\n\n      const imageFiles = files.filter((f) => f.type.startsWith(\"image/\"));\n      const allowedCount = Math.min(imageFiles.length, maxImages - value.length);\n      const filesToUpload = imageFiles.slice(0, allowedCount);\n\n      const previews = filesToUpload.map((f) => URL.createObjectURL(f));\n      setUploadingPreviews(previews);\n\n      if (onUpload) {\n        try {\n          setIsUploading(true);\n          const newImages = await onUpload(filesToUpload);\n          onChange?.([...value, ...newImages]);\n        } catch (err) {\n          console.error(\"Upload failed:\", err);\n        } finally {\n          setIsUploading(false);\n          setUploadingPreviews([]);\n          previews.forEach((p) => URL.revokeObjectURL(p));\n        }\n      }\n\n      e.target.value = \"\";\n    },\n    [value, onChange, onUpload, maxImages]\n  );\n\n  const handleRemove = useCallback(\n    (id: string) => {\n      onRemove?.(id);\n      onChange?.(value.filter((img) => img.id !== id));\n    },\n    [value, onChange, onRemove]\n  );\n\n  return (\n    <div\n      data-slot=\"uploadthing-image-grid\"\n      className={cn(\"w-full\", className)}\n    >\n      <div className={cn(\"grid gap-3\", GRID_COLS[columns])}>\n        {value.map((image) => (\n          <div\n            key={image.id}\n            className=\"relative aspect-square rounded-lg overflow-hidden bg-muted border border-border group\"\n          >\n            <img\n              src={image.url}\n              alt={image.name || \"Uploaded image\"}\n              className=\"w-full h-full object-cover\"\n            />\n            {!disabled && (\n              <button\n                type=\"button\"\n                onClick={() => handleRemove(image.id)}\n                className={cn(\n                  \"absolute top-2 right-2 p-1.5 rounded-full\",\n                  \"bg-black/50 text-white hover:bg-black/70\",\n                  \"opacity-0 group-hover:opacity-100 transition-opacity\"\n                )}\n                aria-label={`Remove ${image.name || \"image\"}`}\n              >\n                <XIcon className=\"w-4 h-4\" />\n              </button>\n            )}\n          </div>\n        ))}\n\n        {uploadingPreviews.map((preview, index) => (\n          <div\n            key={`uploading-${index}`}\n            className=\"relative aspect-square rounded-lg overflow-hidden bg-muted border border-border\"\n          >\n            <img\n              src={preview}\n              alt=\"Uploading\"\n              className=\"w-full h-full object-cover opacity-50\"\n            />\n            <div className=\"absolute inset-0 flex items-center justify-center\">\n              <LoadingSpinner className=\"w-8 h-8 text-primary\" />\n            </div>\n          </div>\n        ))}\n\n        {canAddMore && !isUploading && !disabled && (\n          <label\n            className={cn(\n              \"relative aspect-square rounded-lg border-2 border-dashed border-border\",\n              \"flex flex-col items-center justify-center gap-2 cursor-pointer\",\n              \"hover:border-primary/50 hover:bg-accent/50 transition-colors\"\n            )}\n          >\n            <PlusIcon className=\"w-8 h-8 text-muted-foreground\" />\n            <span className=\"text-xs text-muted-foreground\">\n              {value.length}/{maxImages}\n            </span>\n            <input\n              type=\"file\"\n              accept=\"image/*\"\n              multiple\n              onChange={handleFileChange}\n              className=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n              aria-label=\"Add images\"\n            />\n          </label>\n        )}\n      </div>\n\n      {value.length === 0 && !isUploading && (\n        <p className=\"text-sm text-muted-foreground text-center mt-4\">\n          Click the + button to add images\n        </p>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Grid layout for multiple image uploads. Supports 2, 3, or 4 columns, max image limits, and hover-to-remove functionality.",
  "categories": [
    "uploadthing"
  ],
  "type": "registry:block"
}