{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropzone-tanstack",
  "title": "Dropzone (File Upload)",
  "description": "Displays a control for easier uploading of files directly to Supabase Storage.",
  "dependencies": [
    "react-dropzone",
    "lucide-react",
    "@supabase/ssr@latest",
    "@supabase/supabase-js@latest"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/default/blocks/dropzone/components/dropzone.tsx",
      "content": "'use client'\n\nimport { CheckCircle, File, Loader2, Upload, X } from 'lucide-react'\nimport { createContext, useCallback, useContext, type PropsWithChildren } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { type UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'\nimport { Button } from '@/registry/default/components/ui/button'\n\nexport const formatBytes = (\n  bytes: number,\n  decimals = 2,\n  size?: 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB'\n) => {\n  const k = 1000\n  const dm = decimals < 0 ? 0 : decimals\n  const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\n  if (bytes === 0 || bytes === undefined) return size !== undefined ? `0 ${size}` : '0 bytes'\n  const i = size !== undefined ? sizes.indexOf(size) : Math.floor(Math.log(bytes) / Math.log(k))\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]\n}\n\ntype DropzoneContextType = Omit<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>\n\nconst DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)\n\ntype DropzoneProps = UseSupabaseUploadReturn & {\n  className?: string\n}\n\nconst Dropzone = ({\n  className,\n  children,\n  getRootProps,\n  getInputProps,\n  ...restProps\n}: PropsWithChildren<DropzoneProps>) => {\n  const isSuccess = restProps.isSuccess\n  const isActive = restProps.isDragActive\n  const isInvalid =\n    (restProps.isDragActive && restProps.isDragReject) ||\n    (restProps.errors.length > 0 && !restProps.isSuccess) ||\n    restProps.files.some((file) => file.errors.length !== 0)\n\n  return (\n    <DropzoneContext.Provider value={{ ...restProps }}>\n      <div\n        {...getRootProps({\n          className: cn(\n            'border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground',\n            className,\n            isSuccess ? 'border-solid' : 'border-dashed',\n            isActive && 'border-primary bg-primary/10',\n            isInvalid && 'border-destructive bg-destructive/10'\n          ),\n        })}\n      >\n        <input {...getInputProps()} />\n        {children}\n      </div>\n    </DropzoneContext.Provider>\n  )\n}\nconst DropzoneContent = ({ className }: { className?: string }) => {\n  const {\n    files,\n    setFiles,\n    onUpload,\n    loading,\n    successes,\n    errors,\n    maxFileSize,\n    maxFiles,\n    isSuccess,\n  } = useDropzoneContext()\n\n  const exceedMaxFiles = files.length > maxFiles\n\n  const handleRemoveFile = useCallback(\n    (fileName: string) => {\n      setFiles(files.filter((file) => file.name !== fileName))\n    },\n    [files, setFiles]\n  )\n\n  if (isSuccess) {\n    return (\n      <div className={cn('flex flex-row items-center gap-x-2 justify-center', className)}>\n        <CheckCircle size={16} className=\"text-primary\" />\n        <p className=\"text-primary text-sm\">\n          Successfully uploaded {files.length} file{files.length > 1 ? 's' : ''}\n        </p>\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn('flex flex-col', className)}>\n      {files.map((file, idx) => {\n        const fileError = errors.find((e) => e.name === file.name)\n        const isSuccessfullyUploaded = !!successes.find((e) => e === file.name)\n\n        return (\n          <div\n            key={`${file.name}-${idx}`}\n            className=\"flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 \"\n          >\n            {file.type.startsWith('image/') ? (\n              <div className=\"h-10 w-10 rounded-sm border overflow-hidden shrink-0 bg-muted flex items-center justify-center\">\n                <img src={file.preview} alt={file.name} className=\"object-cover\" />\n              </div>\n            ) : (\n              <div className=\"h-10 w-10 rounded-sm border bg-muted flex items-center justify-center\">\n                <File size={18} />\n              </div>\n            )}\n\n            <div className=\"shrink grow flex flex-col items-start truncate\">\n              <p title={file.name} className=\"text-sm truncate max-w-full\">\n                {file.name}\n              </p>\n              {file.errors.length > 0 ? (\n                <p className=\"text-xs text-destructive\">\n                  {file.errors\n                    .map((e) =>\n                      e.message.startsWith('File is larger than')\n                        ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})`\n                        : e.message\n                    )\n                    .join(', ')}\n                </p>\n              ) : loading && !isSuccessfullyUploaded ? (\n                <p className=\"text-xs text-muted-foreground\">Uploading file...</p>\n              ) : !!fileError ? (\n                <p className=\"text-xs text-destructive\">Failed to upload: {fileError.message}</p>\n              ) : isSuccessfullyUploaded ? (\n                <p className=\"text-xs text-primary\">Successfully uploaded file</p>\n              ) : (\n                <p className=\"text-xs text-muted-foreground\">{formatBytes(file.size, 2)}</p>\n              )}\n            </div>\n\n            {!loading && !isSuccessfullyUploaded && (\n              <Button\n                size=\"icon\"\n                variant=\"link\"\n                className=\"shrink-0 justify-self-end text-muted-foreground hover:text-foreground\"\n                onClick={() => handleRemoveFile(file.name)}\n              >\n                <X />\n              </Button>\n            )}\n          </div>\n        )\n      })}\n      {exceedMaxFiles && (\n        <p className=\"text-sm text-left mt-2 text-destructive\">\n          You may upload only up to {maxFiles} files, please remove {files.length - maxFiles} file\n          {files.length - maxFiles > 1 ? 's' : ''}.\n        </p>\n      )}\n      {files.length > 0 && !exceedMaxFiles && (\n        <div className=\"mt-2\">\n          <Button\n            variant=\"outline\"\n            onClick={onUpload}\n            disabled={files.some((file) => file.errors.length !== 0) || loading}\n          >\n            {loading ? (\n              <>\n                <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                Uploading...\n              </>\n            ) : (\n              <>Upload files</>\n            )}\n          </Button>\n        </div>\n      )}\n    </div>\n  )\n}\n\nconst DropzoneEmptyState = ({ className }: { className?: string }) => {\n  const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext()\n\n  if (isSuccess) {\n    return null\n  }\n\n  return (\n    <div className={cn('flex flex-col items-center gap-y-2', className)}>\n      <Upload size={20} className=\"text-muted-foreground\" />\n      <p className=\"text-sm\">\n        Upload{!!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : ''} file\n        {!maxFiles || maxFiles > 1 ? 's' : ''}\n      </p>\n      <div className=\"flex flex-col items-center gap-y-1\">\n        <p className=\"text-xs text-muted-foreground\">\n          Drag and drop or{' '}\n          <a\n            onClick={() => inputRef.current?.click()}\n            className=\"underline cursor-pointer transition hover:text-foreground\"\n          >\n            select {maxFiles === 1 ? `file` : 'files'}\n          </a>{' '}\n          to upload\n        </p>\n        {maxFileSize !== Number.POSITIVE_INFINITY && (\n          <p className=\"text-xs text-muted-foreground\">\n            Maximum file size: {formatBytes(maxFileSize, 2)}\n          </p>\n        )}\n      </div>\n    </div>\n  )\n}\n\nconst useDropzoneContext = () => {\n  const context = useContext(DropzoneContext)\n\n  if (!context) {\n    throw new Error('useDropzoneContext must be used within a Dropzone')\n  }\n\n  return context\n}\n\nexport { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext }\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/dropzone/hooks/use-supabase-upload.ts",
      "content": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDropzone, type FileError, type FileRejection } from 'react-dropzone'\n\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\n\nconst supabase = createClient()\n\ninterface FileWithPreview extends File {\n  preview?: string\n  errors: readonly FileError[]\n}\n\ntype UseSupabaseUploadOptions = {\n  /**\n   * Name of bucket to upload files to in your Supabase project\n   */\n  bucketName: string\n  /**\n   * Folder to upload files to in the specified bucket within your Supabase project.\n   *\n   * Defaults to uploading files to the root of the bucket\n   *\n   * e.g If specified path is `test`, your file will be uploaded as `test/file_name`\n   */\n  path?: string\n  /**\n   * Allowed MIME types for each file upload (e.g `image/png`, `text/html`, etc). Wildcards are also supported (e.g `image/*`).\n   *\n   * Defaults to allowing uploading of all MIME types.\n   */\n  allowedMimeTypes?: string[]\n  /**\n   * Maximum upload size of each file allowed in bytes. (e.g 1000 bytes = 1 KB)\n   */\n  maxFileSize?: number\n  /**\n   * Maximum number of files allowed per upload.\n   */\n  maxFiles?: number\n  /**\n   * The number of seconds the asset is cached in the browser and in the Supabase CDN.\n   *\n   * This is set in the Cache-Control: max-age=<seconds> header. Defaults to 3600 seconds.\n   */\n  cacheControl?: number\n  /**\n   * When set to true, the file is overwritten if it exists.\n   *\n   * When set to false, an error is thrown if the object already exists. Defaults to `false`\n   */\n  upsert?: boolean\n}\n\ntype UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>\n\nconst useSupabaseUpload = (options: UseSupabaseUploadOptions) => {\n  const {\n    bucketName,\n    path,\n    allowedMimeTypes = [],\n    maxFileSize = Number.POSITIVE_INFINITY,\n    maxFiles = 1,\n    cacheControl = 3600,\n    upsert = false,\n  } = options\n\n  const [files, setFiles] = useState<FileWithPreview[]>([])\n  const [loading, setLoading] = useState<boolean>(false)\n  const [errors, setErrors] = useState<{ name: string; message: string }[]>([])\n  const [successes, setSuccesses] = useState<string[]>([])\n\n  const isSuccess = useMemo(() => {\n    if (errors.length === 0 && successes.length === 0) {\n      return false\n    }\n    if (errors.length === 0 && successes.length === files.length) {\n      return true\n    }\n    return false\n  }, [errors.length, successes.length, files.length])\n\n  const onDrop = useCallback(\n    (acceptedFiles: File[], fileRejections: FileRejection[]) => {\n      const validFiles = acceptedFiles\n        .filter((file) => !files.find((x) => x.name === file.name))\n        .map((file) => {\n          ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n          ;(file as FileWithPreview).errors = []\n          return file as FileWithPreview\n        })\n\n      const invalidFiles = fileRejections.map(({ file, errors }) => {\n        ;(file as FileWithPreview).preview = URL.createObjectURL(file)\n        ;(file as FileWithPreview).errors = errors\n        return file as FileWithPreview\n      })\n\n      const newFiles = [...files, ...validFiles, ...invalidFiles]\n\n      setFiles(newFiles)\n    },\n    [files, setFiles]\n  )\n\n  const dropzoneProps = useDropzone({\n    onDrop,\n    noClick: true,\n    accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),\n    maxSize: maxFileSize,\n    maxFiles: maxFiles,\n    multiple: maxFiles !== 1,\n  })\n\n  const onUpload = useCallback(async () => {\n    setLoading(true)\n\n    // [Joshen] This is to support handling partial successes\n    // If any files didn't upload for any reason, hitting \"Upload\" again will only upload the files that had errors\n    const filesWithErrors = errors.map((x) => x.name)\n    const filesToUpload =\n      filesWithErrors.length > 0\n        ? [\n            ...files.filter((f) => filesWithErrors.includes(f.name)),\n            ...files.filter((f) => !successes.includes(f.name)),\n          ]\n        : files\n\n    const responses = await Promise.all(\n      filesToUpload.map(async (file) => {\n        const { error } = await supabase.storage\n          .from(bucketName)\n          .upload(!!path ? `${path}/${file.name}` : file.name, file, {\n            cacheControl: cacheControl.toString(),\n            upsert,\n          })\n        if (error) {\n          return { name: file.name, message: error.message }\n        } else {\n          return { name: file.name, message: undefined }\n        }\n      })\n    )\n\n    const responseErrors = responses.filter((x) => x.message !== undefined)\n    // if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.\n    setErrors(responseErrors)\n\n    const responseSuccesses = responses.filter((x) => x.message === undefined)\n    const newSuccesses = Array.from(\n      new Set([...successes, ...responseSuccesses.map((x) => x.name)])\n    )\n    setSuccesses(newSuccesses)\n\n    setLoading(false)\n  }, [files, path, bucketName, errors, successes])\n\n  useEffect(() => {\n    if (files.length === 0) {\n      setErrors([])\n    }\n\n    // If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file\n    if (files.length <= maxFiles) {\n      let changed = false\n      const newFiles = files.map((file) => {\n        if (file.errors.some((e) => e.code === 'too-many-files')) {\n          file.errors = file.errors.filter((e) => e.code !== 'too-many-files')\n          changed = true\n        }\n        return file\n      })\n      if (changed) {\n        setFiles(newFiles)\n      }\n    }\n  }, [files.length, setFiles, maxFiles])\n\n  return {\n    files,\n    setFiles,\n    successes,\n    isSuccess,\n    loading,\n    errors,\n    setErrors,\n    onUpload,\n    maxFileSize: maxFileSize,\n    maxFiles: maxFiles,\n    allowedMimeTypes,\n    ...dropzoneProps,\n  }\n}\n\nexport { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/clients/tanstack/lib/supabase/client.ts",
      "content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n  return createBrowserClient(\n    import.meta.env.VITE_SUPABASE_URL!,\n    import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n  )\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/tanstack/lib/supabase/server.ts",
      "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n  return createServerClient(\n    process.env.VITE_SUPABASE_URL!,\n    process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return Object.entries(getCookies()).map(\n            ([name, value]) =>\n              ({\n                name,\n                value,\n              }) as { name: string; value: string }\n          )\n        },\n        setAll(cookies) {\n          cookies.forEach((cookie) => {\n            setCookie(cookie.name, cookie.value)\n          })\n        },\n      },\n    }\n  )\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "VITE_SUPABASE_URL": "",
    "VITE_SUPABASE_PUBLISHABLE_KEY": ""
  },
  "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
  "type": "registry:component"
}