{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"file-upload","type":"registry:ui","title":"FileUpload","description":"FileUpload from react-fancy","package":"react-fancy","dependencies":["lucide-react"],"registryDependencies":[],"files":[{"path":"components\/fancy\/file-upload\/FileUpload.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { FileUploadContextValue } from \".\/FileUpload.types\";\r\n\r\nexport const FileUploadContext =\r\n  createContext<FileUploadContextValue | null>(null);\r\n\r\nexport function useFileUpload(): FileUploadContextValue {\r\n  const ctx = useContext(FileUploadContext);\r\n  if (!ctx) {\r\n    throw new Error(\r\n      \"FileUpload compound components must be used within <FileUpload>\",\r\n    );\r\n  }\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/file-upload\/FileUpload.context.ts"},{"path":"components\/fancy\/file-upload\/FileUpload.tsx","content":"import { useCallback, useMemo, useState } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { FileUploadContext } from \".\/FileUpload.context\";\r\nimport { FileUploadDropzone } from \".\/FileUploadDropzone\";\r\nimport { FileUploadList } from \".\/FileUploadList\";\r\nimport type { FileUploadProps } from \".\/FileUpload.types\";\r\n\r\nfunction FileUploadRoot({\r\n  children,\r\n  value,\r\n  onChange,\r\n  maxFiles,\r\n  maxSize,\r\n  disabled = false,\r\n  className,\r\n}: FileUploadProps) {\r\n  const [internalFiles, setInternalFiles] = useState<File[]>([]);\r\n  const files = value ?? internalFiles;\r\n\r\n  const addFiles = useCallback(\r\n    (newFiles: FileList | File[]) => {\r\n      let toAdd = Array.from(newFiles);\r\n\r\n      if (maxSize) {\r\n        toAdd = toAdd.filter((f) => f.size <= maxSize);\r\n      }\r\n\r\n      const updated = [...files, ...toAdd];\r\n      const limited = maxFiles ? updated.slice(0, maxFiles) : updated;\r\n\r\n      if (value === undefined) setInternalFiles(limited);\r\n      onChange?.(limited);\r\n    },\r\n    [files, maxFiles, maxSize, onChange, value],\r\n  );\r\n\r\n  const removeFile = useCallback(\r\n    (index: number) => {\r\n      const updated = files.filter((_, i) => i !== index);\r\n      if (value === undefined) setInternalFiles(updated);\r\n      onChange?.(updated);\r\n    },\r\n    [files, onChange, value],\r\n  );\r\n\r\n  const ctx = useMemo(\r\n    () => ({ files, addFiles, removeFile, disabled }),\r\n    [files, addFiles, removeFile, disabled],\r\n  );\r\n\r\n  return (\r\n    <FileUploadContext.Provider value={ctx}>\r\n      <div data-react-fancy-file-upload=\"\" className={cn(className)}>{children}<\/div>\r\n    <\/FileUploadContext.Provider>\r\n  );\r\n}\r\n\r\nexport const FileUpload = Object.assign(FileUploadRoot, {\r\n  Dropzone: FileUploadDropzone,\r\n  List: FileUploadList,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/file-upload\/FileUpload.tsx"},{"path":"components\/fancy\/file-upload\/FileUpload.types.ts","content":"import type { ReactNode } from \"react\";\r\n\r\nexport interface FileUploadContextValue {\r\n  files: File[];\r\n  addFiles: (files: FileList | File[]) => void;\r\n  removeFile: (index: number) => void;\r\n  disabled: boolean;\r\n}\r\n\r\nexport interface FileUploadProps {\r\n  children: ReactNode;\r\n  value?: File[];\r\n  onChange?: (files: File[]) => void;\r\n  accept?: string;\r\n  multiple?: boolean;\r\n  maxFiles?: number;\r\n  maxSize?: number;\r\n  disabled?: boolean;\r\n  className?: string;\r\n}\r\n\r\nexport interface FileUploadDropzoneProps {\r\n  children?: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface FileUploadListProps {\r\n  thumbnail?: boolean;\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/file-upload\/FileUpload.types.ts"},{"path":"components\/fancy\/file-upload\/FileUploadDropzone.tsx","content":"import { useCallback, useRef, useState } from \"react\";\r\nimport { Upload } from \"lucide-react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { useFileUpload } from \".\/FileUpload.context\";\r\nimport type { FileUploadDropzoneProps } from \".\/FileUpload.types\";\r\n\r\nexport function FileUploadDropzone({\r\n  children,\r\n  className,\r\n}: FileUploadDropzoneProps) {\r\n  const { addFiles, disabled } = useFileUpload();\r\n  const [dragOver, setDragOver] = useState(false);\r\n  const inputRef = useRef<HTMLInputElement>(null);\r\n\r\n  const handleDrop = useCallback(\r\n    (e: React.DragEvent) => {\r\n      e.preventDefault();\r\n      setDragOver(false);\r\n      if (disabled) return;\r\n      addFiles(e.dataTransfer.files);\r\n    },\r\n    [addFiles, disabled],\r\n  );\r\n\r\n  const handleDragOver = useCallback((e: React.DragEvent) => {\r\n    e.preventDefault();\r\n    setDragOver(true);\r\n  }, []);\r\n\r\n  const handleDragLeave = useCallback(() => setDragOver(false), []);\r\n\r\n  return (\r\n    <div\r\n      data-react-fancy-file-upload-dropzone=\"\"\r\n      onDrop={handleDrop}\r\n      onDragOver={handleDragOver}\r\n      onDragLeave={handleDragLeave}\r\n      onClick={() => !disabled && inputRef.current?.click()}\r\n      className={cn(\r\n        \"flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-8 text-center transition-colors\",\r\n        dragOver\r\n          ? \"border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-950\"\r\n          : \"border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-500\",\r\n        disabled && \"cursor-not-allowed opacity-50\",\r\n        className,\r\n      )}\r\n    >\r\n      <input\r\n        ref={inputRef}\r\n        type=\"file\"\r\n        className=\"hidden\"\r\n        onChange={(e) => e.target.files && addFiles(e.target.files)}\r\n        multiple\r\n        disabled={disabled}\r\n      \/>\r\n      {children ?? (\r\n        <>\r\n          <Upload size={32} strokeWidth={1.5} className=\"mb-2 text-zinc-400\" \/>\r\n          <p className=\"text-sm text-zinc-500\">\r\n            Drop files here or click to browse\r\n          <\/p>\r\n        <\/>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nFileUploadDropzone.displayName = \"FileUploadDropzone\";\r\n","type":"registry:ui","target":"components\/fancy\/file-upload\/FileUploadDropzone.tsx"},{"path":"components\/fancy\/file-upload\/FileUploadList.tsx","content":"import { useEffect, useMemo } from \"react\";\r\nimport { File, X } from \"lucide-react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { useFileUpload } from \".\/FileUpload.context\";\r\nimport type { FileUploadListProps } from \".\/FileUpload.types\";\r\n\r\nfunction formatSize(bytes: number): string {\r\n  if (bytes < 1024) return `${bytes} B`;\r\n  if (bytes < 1024 * 1024) return `${(bytes \/ 1024).toFixed(1)} KB`;\r\n  return `${(bytes \/ (1024 * 1024)).toFixed(1)} MB`;\r\n}\r\n\r\nfunction getExtension(name: string): string {\r\n  const parts = name.split(\".\");\r\n  return parts.length > 1 ? parts.pop()!.toUpperCase() : \"\";\r\n}\r\n\r\nfunction ThumbnailItem({ file, index, onRemove }: { file: File; index: number; onRemove: (i: number) => void }) {\r\n  const isImage = file.type.startsWith(\"image\/\");\r\n  const objectUrl = useMemo(() => (isImage ? URL.createObjectURL(file) : null), [file, isImage]);\r\n\r\n  useEffect(() => {\r\n    return () => {\r\n      if (objectUrl) URL.revokeObjectURL(objectUrl);\r\n    };\r\n  }, [objectUrl]);\r\n\r\n  return (\r\n    <div className=\"group relative flex w-20 flex-col items-center gap-1\">\r\n      <div className=\"relative h-20 w-20 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800\">\r\n        {isImage && objectUrl ? (\r\n          <img src={objectUrl} alt={file.name} className=\"h-full w-full object-cover\" \/>\r\n        ) : (\r\n          <div className=\"flex h-full w-full flex-col items-center justify-center gap-1\">\r\n            <File size={20} className=\"text-zinc-400\" \/>\r\n            <span className=\"text-[10px] font-medium text-zinc-500\">{getExtension(file.name)}<\/span>\r\n          <\/div>\r\n        )}\r\n        <button\r\n          type=\"button\"\r\n          onClick={() => onRemove(index)}\r\n          className=\"absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-black\/60 text-white opacity-0 transition-opacity hover:bg-black\/80 group-hover:opacity-100\"\r\n          aria-label={`Remove ${file.name}`}\r\n        >\r\n          <X size={10} \/>\r\n        <\/button>\r\n      <\/div>\r\n      <span className=\"w-full truncate text-center text-xs text-zinc-500\">{file.name}<\/span>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nexport function FileUploadList({ thumbnail, className }: FileUploadListProps) {\r\n  const { files, removeFile } = useFileUpload();\r\n\r\n  if (files.length === 0) return null;\r\n\r\n  if (thumbnail) {\r\n    return (\r\n      <div data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 flex flex-wrap gap-3\", className)}>\r\n        {files.map((file, i) => (\r\n          <ThumbnailItem key={`${file.name}-${i}`} file={file} index={i} onRemove={removeFile} \/>\r\n        ))}\r\n      <\/div>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <ul data-react-fancy-file-upload-list=\"\" className={cn(\"mt-3 space-y-2\", className)}>\r\n      {files.map((file, i) => (\r\n        <li\r\n          key={`${file.name}-${i}`}\r\n          className=\"flex items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700\"\r\n        >\r\n          <File size={16} className=\"shrink-0 text-zinc-400\" \/>\r\n          <span className=\"flex-1 truncate\">{file.name}<\/span>\r\n          <span className=\"shrink-0 text-xs text-zinc-400\">\r\n            {formatSize(file.size)}\r\n          <\/span>\r\n          <button\r\n            type=\"button\"\r\n            onClick={() => removeFile(i)}\r\n            className=\"shrink-0 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300\"\r\n            aria-label={`Remove ${file.name}`}\r\n          >\r\n            <X size={14} \/>\r\n          <\/button>\r\n        <\/li>\r\n      ))}\r\n    <\/ul>\r\n  );\r\n}\r\n\r\nFileUploadList.displayName = \"FileUploadList\";\r\n","type":"registry:ui","target":"components\/fancy\/file-upload\/FileUploadList.tsx"},{"path":"components\/fancy\/file-upload\/index.ts","content":"export { FileUpload } from \".\/FileUpload\";\r\nexport { useFileUpload } from \".\/FileUpload.context\";\r\nexport type {\r\n  FileUploadProps,\r\n  FileUploadDropzoneProps,\r\n  FileUploadListProps,\r\n  FileUploadContextValue,\r\n} from \".\/FileUpload.types\";\r\n","type":"registry:ui","target":"components\/fancy\/file-upload\/index.ts"}]}