{
  "name": "file-upload",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-as-ref",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/file-upload.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  FileArchiveIcon,\n  FileAudioIcon,\n  FileCodeIcon,\n  FileCogIcon,\n  FileIcon,\n  FileTextIcon,\n  FileVideoIcon,\n} from \"lucide-react\";\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"FileUpload\";\nconst DROPZONE_NAME = \"FileUploadDropzone\";\nconst TRIGGER_NAME = \"FileUploadTrigger\";\nconst LIST_NAME = \"FileUploadList\";\nconst ITEM_NAME = \"FileUploadItem\";\nconst ITEM_PREVIEW_NAME = \"FileUploadItemPreview\";\nconst ITEM_METADATA_NAME = \"FileUploadItemMetadata\";\nconst ITEM_PROGRESS_NAME = \"FileUploadItemProgress\";\nconst ITEM_DELETE_NAME = \"FileUploadItemDelete\";\nconst CLEAR_NAME = \"FileUploadClear\";\n\nfunction formatBytes(bytes: number) {\n  if (bytes === 0) return \"0 B\";\n  const sizes = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  const i = Math.floor(Math.log(bytes) / Math.log(1024));\n  return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${sizes[i]}`;\n}\n\nfunction getFileIcon(file: File) {\n  const type = file.type;\n  const extension = file.name.split(\".\").pop()?.toLowerCase() ?? \"\";\n\n  if (type.startsWith(\"video/\")) {\n    return <FileVideoIcon />;\n  }\n\n  if (type.startsWith(\"audio/\")) {\n    return <FileAudioIcon />;\n  }\n\n  if (\n    type.startsWith(\"text/\") ||\n    [\"txt\", \"md\", \"rtf\", \"pdf\"].includes(extension)\n  ) {\n    return <FileTextIcon />;\n  }\n\n  if (\n    [\n      \"html\",\n      \"css\",\n      \"js\",\n      \"jsx\",\n      \"ts\",\n      \"tsx\",\n      \"json\",\n      \"xml\",\n      \"php\",\n      \"py\",\n      \"rb\",\n      \"java\",\n      \"c\",\n      \"cpp\",\n      \"cs\",\n    ].includes(extension)\n  ) {\n    return <FileCodeIcon />;\n  }\n\n  if ([\"zip\", \"rar\", \"7z\", \"tar\", \"gz\", \"bz2\"].includes(extension)) {\n    return <FileArchiveIcon />;\n  }\n\n  if (\n    [\"exe\", \"msi\", \"app\", \"apk\", \"deb\", \"rpm\"].includes(extension) ||\n    type.startsWith(\"application/\")\n  ) {\n    return <FileCogIcon />;\n  }\n\n  return <FileIcon />;\n}\n\ntype Direction = \"ltr\" | \"rtl\";\n\ninterface FileState {\n  file: File;\n  progress: number;\n  error?: string;\n  status: \"idle\" | \"uploading\" | \"error\" | \"success\";\n}\n\ninterface StoreState {\n  files: Map<File, FileState>;\n  dragOver: boolean;\n  invalid: boolean;\n}\n\ntype StoreAction =\n  | { type: \"ADD_FILES\"; files: File[] }\n  | { type: \"SET_FILES\"; files: File[] }\n  | { type: \"SET_PROGRESS\"; file: File; progress: number }\n  | { type: \"SET_SUCCESS\"; file: File }\n  | { type: \"SET_ERROR\"; file: File; error: string }\n  | { type: \"REMOVE_FILE\"; file: File }\n  | { type: \"SET_DRAG_OVER\"; dragOver: boolean }\n  | { type: \"SET_INVALID\"; invalid: boolean }\n  | { type: \"CLEAR\" };\n\ntype Store = {\n  getState: () => StoreState;\n  dispatch: (action: StoreAction) => void;\n  subscribe: (listener: () => void) => () => void;\n};\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nfunction useStore<T>(selector: (state: StoreState) => T): T {\n  const store = useStoreContext(\"useStore\");\n\n  const lastValueRef = useLazyRef<{ value: T; state: StoreState } | null>(\n    () => null,\n  );\n\n  const getSnapshot = React.useCallback(() => {\n    const state = store.getState();\n    const prevValue = lastValueRef.current;\n\n    if (prevValue && prevValue.state === state) {\n      return prevValue.value;\n    }\n\n    const nextValue = selector(state);\n    lastValueRef.current = { value: nextValue, state };\n    return nextValue;\n  }, [store, selector, lastValueRef]);\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface FileUploadContextValue {\n  inputId: string;\n  dropzoneId: string;\n  listId: string;\n  labelId: string;\n  disabled: boolean;\n  dir: Direction;\n  inputRef: React.RefObject<HTMLInputElement | null>;\n  urlCache: WeakMap<File, string>;\n}\n\nconst FileUploadContext = React.createContext<FileUploadContextValue | null>(\n  null,\n);\n\nfunction useFileUploadContext(consumerName: string) {\n  const context = React.useContext(FileUploadContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface FileUploadProps\n  extends Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> {\n  value?: File[];\n  defaultValue?: File[];\n  onValueChange?: (files: File[]) => void;\n  onAccept?: (files: File[]) => void;\n  onFileAccept?: (file: File) => void;\n  onFileReject?: (file: File, message: string) => void;\n  onFileValidate?: (file: File) => string | null | undefined;\n  onUpload?: (\n    files: File[],\n    options: {\n      onProgress: (file: File, progress: number) => void;\n      onSuccess: (file: File) => void;\n      onError: (file: File, error: Error) => void;\n    },\n  ) => Promise<void> | void;\n  accept?: string;\n  maxFiles?: number;\n  maxSize?: number;\n  dir?: Direction;\n  label?: string;\n  name?: string;\n  asChild?: boolean;\n  disabled?: boolean;\n  invalid?: boolean;\n  multiple?: boolean;\n  required?: boolean;\n}\n\nfunction FileUpload(props: FileUploadProps) {\n  const {\n    value,\n    defaultValue,\n    onValueChange,\n    onAccept,\n    onFileAccept,\n    onFileReject,\n    onFileValidate,\n    onUpload,\n    accept,\n    maxFiles,\n    maxSize,\n    dir: dirProp,\n    label,\n    name,\n    asChild,\n    disabled = false,\n    invalid = false,\n    multiple = false,\n    required = false,\n    children,\n    className,\n    ...rootProps\n  } = props;\n\n  const inputId = React.useId();\n  const dropzoneId = React.useId();\n  const listId = React.useId();\n  const labelId = React.useId();\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n  const listeners = useLazyRef(() => new Set<() => void>()).current;\n  const files = useLazyRef<Map<File, FileState>>(() => new Map()).current;\n  const urlCache = useLazyRef(() => new WeakMap<File, string>()).current;\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const isControlled = value !== undefined;\n\n  const propsRef = useAsRef({\n    onValueChange,\n    onAccept,\n    onFileAccept,\n    onFileReject,\n    onFileValidate,\n    onUpload,\n  });\n\n  const store = React.useMemo<Store>(() => {\n    let state: StoreState = {\n      files,\n      dragOver: false,\n      invalid: invalid,\n    };\n\n    function reducer(state: StoreState, action: StoreAction): StoreState {\n      switch (action.type) {\n        case \"ADD_FILES\": {\n          for (const file of action.files) {\n            files.set(file, {\n              file,\n              progress: 0,\n              status: \"idle\",\n            });\n          }\n\n          if (propsRef.current.onValueChange) {\n            const fileList = Array.from(files.values()).map(\n              (fileState) => fileState.file,\n            );\n            propsRef.current.onValueChange(fileList);\n          }\n          return { ...state, files };\n        }\n\n        case \"SET_FILES\": {\n          const newFileSet = new Set(action.files);\n          for (const existingFile of files.keys()) {\n            if (!newFileSet.has(existingFile)) {\n              files.delete(existingFile);\n            }\n          }\n\n          for (const file of action.files) {\n            const existingState = files.get(file);\n            if (!existingState) {\n              files.set(file, {\n                file,\n                progress: 0,\n                status: \"idle\",\n              });\n            }\n          }\n          return { ...state, files };\n        }\n\n        case \"SET_PROGRESS\": {\n          const fileState = files.get(action.file);\n          if (fileState) {\n            files.set(action.file, {\n              ...fileState,\n              progress: action.progress,\n              status: \"uploading\",\n            });\n          }\n          return { ...state, files };\n        }\n\n        case \"SET_SUCCESS\": {\n          const fileState = files.get(action.file);\n          if (fileState) {\n            files.set(action.file, {\n              ...fileState,\n              progress: 100,\n              status: \"success\",\n            });\n          }\n          return { ...state, files };\n        }\n\n        case \"SET_ERROR\": {\n          const fileState = files.get(action.file);\n          if (fileState) {\n            files.set(action.file, {\n              ...fileState,\n              error: action.error,\n              status: \"error\",\n            });\n          }\n          return { ...state, files };\n        }\n\n        case \"REMOVE_FILE\": {\n          const cachedUrl = urlCache.get(action.file);\n          if (cachedUrl) {\n            URL.revokeObjectURL(cachedUrl);\n            urlCache.delete(action.file);\n          }\n\n          files.delete(action.file);\n\n          if (propsRef.current.onValueChange) {\n            const fileList = Array.from(files.values()).map(\n              (fileState) => fileState.file,\n            );\n            propsRef.current.onValueChange(fileList);\n          }\n          return { ...state, files };\n        }\n\n        case \"SET_DRAG_OVER\": {\n          return { ...state, dragOver: action.dragOver };\n        }\n\n        case \"SET_INVALID\": {\n          return { ...state, invalid: action.invalid };\n        }\n\n        case \"CLEAR\": {\n          for (const file of files.keys()) {\n            const cachedUrl = urlCache.get(file);\n            if (cachedUrl) {\n              URL.revokeObjectURL(cachedUrl);\n              urlCache.delete(file);\n            }\n          }\n\n          files.clear();\n          if (propsRef.current.onValueChange) {\n            propsRef.current.onValueChange([]);\n          }\n          return { ...state, files, invalid: false };\n        }\n\n        default:\n          return state;\n      }\n    }\n\n    return {\n      getState: () => state,\n      dispatch: (action) => {\n        state = reducer(state, action);\n        for (const listener of listeners) {\n          listener();\n        }\n      },\n      subscribe: (listener) => {\n        listeners.add(listener);\n        return () => listeners.delete(listener);\n      },\n    };\n  }, [listeners, files, invalid, propsRef, urlCache]);\n\n  const acceptTypes = React.useMemo(\n    () => accept?.split(\",\").map((t) => t.trim()) ?? null,\n    [accept],\n  );\n\n  const onProgress = useLazyRef(() => {\n    let frame = 0;\n    return (file: File, progress: number) => {\n      if (frame) return;\n      frame = requestAnimationFrame(() => {\n        frame = 0;\n        store.dispatch({\n          type: \"SET_PROGRESS\",\n          file,\n          progress: Math.min(Math.max(0, progress), 100),\n        });\n      });\n    };\n  }).current;\n\n  React.useEffect(() => {\n    if (isControlled) {\n      store.dispatch({ type: \"SET_FILES\", files: value });\n    } else if (\n      defaultValue &&\n      defaultValue.length > 0 &&\n      !store.getState().files.size\n    ) {\n      store.dispatch({ type: \"SET_FILES\", files: defaultValue });\n    }\n  }, [value, defaultValue, isControlled, store]);\n\n  React.useEffect(() => {\n    return () => {\n      for (const file of files.keys()) {\n        const cachedUrl = urlCache.get(file);\n        if (cachedUrl) {\n          URL.revokeObjectURL(cachedUrl);\n        }\n      }\n    };\n  }, [files, urlCache]);\n\n  const onFilesUpload = React.useCallback(\n    async (files: File[]) => {\n      try {\n        for (const file of files) {\n          store.dispatch({ type: \"SET_PROGRESS\", file, progress: 0 });\n        }\n\n        if (propsRef.current.onUpload) {\n          await propsRef.current.onUpload(files, {\n            onProgress,\n            onSuccess: (file) => {\n              store.dispatch({ type: \"SET_SUCCESS\", file });\n            },\n            onError: (file, error) => {\n              store.dispatch({\n                type: \"SET_ERROR\",\n                file,\n                error: error.message ?? \"Upload failed\",\n              });\n            },\n          });\n        } else {\n          for (const file of files) {\n            store.dispatch({ type: \"SET_SUCCESS\", file });\n          }\n        }\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : \"Upload failed\";\n        for (const file of files) {\n          store.dispatch({\n            type: \"SET_ERROR\",\n            file,\n            error: errorMessage,\n          });\n        }\n      }\n    },\n    [store, propsRef, onProgress],\n  );\n\n  const onFilesChange = React.useCallback(\n    (originalFiles: File[]) => {\n      if (disabled) return;\n\n      let filesToProcess = [...originalFiles];\n      let invalid = false;\n\n      if (maxFiles) {\n        const currentCount = store.getState().files.size;\n        const remainingSlotCount = Math.max(0, maxFiles - currentCount);\n\n        if (remainingSlotCount < filesToProcess.length) {\n          const rejectedFiles = filesToProcess.slice(remainingSlotCount);\n          invalid = true;\n\n          filesToProcess = filesToProcess.slice(0, remainingSlotCount);\n\n          for (const file of rejectedFiles) {\n            let rejectionMessage = `Maximum ${maxFiles} files allowed`;\n\n            if (propsRef.current.onFileValidate) {\n              const validationMessage = propsRef.current.onFileValidate(file);\n              if (validationMessage) {\n                rejectionMessage = validationMessage;\n              }\n            }\n\n            propsRef.current.onFileReject?.(file, rejectionMessage);\n          }\n        }\n      }\n\n      const acceptedFiles: File[] = [];\n      const rejectedFiles: { file: File; message: string }[] = [];\n\n      for (const file of filesToProcess) {\n        let rejected = false;\n        let rejectionMessage = \"\";\n\n        if (propsRef.current.onFileValidate) {\n          const validationMessage = propsRef.current.onFileValidate(file);\n          if (validationMessage) {\n            rejectionMessage = validationMessage;\n            propsRef.current.onFileReject?.(file, rejectionMessage);\n            rejected = true;\n            invalid = true;\n            continue;\n          }\n        }\n\n        if (acceptTypes) {\n          const fileType = file.type;\n          const fileExtension = `.${file.name.split(\".\").pop()}`;\n\n          if (\n            !acceptTypes.some(\n              (type) =>\n                type === fileType ||\n                type === fileExtension ||\n                (type.includes(\"/*\") &&\n                  fileType.startsWith(type.replace(\"/*\", \"/\"))),\n            )\n          ) {\n            rejectionMessage = \"File type not accepted\";\n            propsRef.current.onFileReject?.(file, rejectionMessage);\n            rejected = true;\n            invalid = true;\n          }\n        }\n\n        if (maxSize && file.size > maxSize) {\n          rejectionMessage = \"File too large\";\n          propsRef.current.onFileReject?.(file, rejectionMessage);\n          rejected = true;\n          invalid = true;\n        }\n\n        if (!rejected) {\n          acceptedFiles.push(file);\n        } else {\n          rejectedFiles.push({ file, message: rejectionMessage });\n        }\n      }\n\n      if (invalid) {\n        store.dispatch({ type: \"SET_INVALID\", invalid });\n        setTimeout(() => {\n          store.dispatch({ type: \"SET_INVALID\", invalid: false });\n        }, 2000);\n      }\n\n      if (acceptedFiles.length > 0) {\n        store.dispatch({ type: \"ADD_FILES\", files: acceptedFiles });\n\n        if (isControlled && propsRef.current.onValueChange) {\n          const currentFiles = Array.from(store.getState().files.values()).map(\n            (f) => f.file,\n          );\n          propsRef.current.onValueChange([...currentFiles]);\n        }\n\n        if (propsRef.current.onAccept) {\n          propsRef.current.onAccept(acceptedFiles);\n        }\n\n        for (const file of acceptedFiles) {\n          propsRef.current.onFileAccept?.(file);\n        }\n\n        if (propsRef.current.onUpload) {\n          requestAnimationFrame(() => {\n            onFilesUpload(acceptedFiles);\n          });\n        }\n      }\n    },\n    [\n      store,\n      isControlled,\n      propsRef,\n      onFilesUpload,\n      maxFiles,\n      acceptTypes,\n      maxSize,\n      disabled,\n    ],\n  );\n\n  const onInputChange = React.useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      const files = Array.from(event.target.files ?? []);\n      onFilesChange(files);\n      event.target.value = \"\";\n    },\n    [onFilesChange],\n  );\n\n  const contextValue = React.useMemo<FileUploadContextValue>(\n    () => ({\n      dropzoneId,\n      inputId,\n      listId,\n      labelId,\n      dir,\n      disabled,\n      inputRef,\n      urlCache,\n    }),\n    [dropzoneId, inputId, listId, labelId, dir, disabled, urlCache],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <FileUploadContext.Provider value={contextValue}>\n        <RootPrimitive\n          data-disabled={disabled ? \"\" : undefined}\n          data-slot=\"file-upload\"\n          dir={dir}\n          {...rootProps}\n          className={cn(\"relative flex flex-col gap-2\", className)}\n        >\n          {children}\n          <input\n            type=\"file\"\n            id={inputId}\n            aria-labelledby={labelId}\n            aria-describedby={dropzoneId}\n            ref={inputRef}\n            tabIndex={-1}\n            accept={accept}\n            name={name}\n            className=\"sr-only\"\n            disabled={disabled}\n            multiple={multiple}\n            required={required}\n            onChange={onInputChange}\n          />\n          <div id={labelId} className=\"sr-only\">\n            {label ?? \"File upload\"}\n          </div>\n        </RootPrimitive>\n      </FileUploadContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface FileUploadDropzoneProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\nfunction FileUploadDropzone(props: FileUploadDropzoneProps) {\n  const {\n    asChild,\n    className,\n    onClick: onClickProp,\n    onDragOver: onDragOverProp,\n    onDragEnter: onDragEnterProp,\n    onDragLeave: onDragLeaveProp,\n    onDrop: onDropProp,\n    onPaste: onPasteProp,\n    onKeyDown: onKeyDownProp,\n    ...dropzoneProps\n  } = props;\n\n  const context = useFileUploadContext(DROPZONE_NAME);\n  const store = useStoreContext(DROPZONE_NAME);\n  const dragOver = useStore((state) => state.dragOver);\n  const invalid = useStore((state) => state.invalid);\n\n  const propsRef = useAsRef({\n    onClick: onClickProp,\n    onDragOver: onDragOverProp,\n    onDragEnter: onDragEnterProp,\n    onDragLeave: onDragLeaveProp,\n    onDrop: onDropProp,\n    onPaste: onPasteProp,\n    onKeyDown: onKeyDownProp,\n  });\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<HTMLDivElement>) => {\n      propsRef.current.onClick?.(event);\n\n      if (event.defaultPrevented) return;\n\n      const target = event.target;\n\n      const isFromTrigger =\n        target instanceof HTMLElement &&\n        target.closest('[data-slot=\"file-upload-trigger\"]');\n\n      if (!isFromTrigger) {\n        context.inputRef.current?.click();\n      }\n    },\n    [context.inputRef, propsRef],\n  );\n\n  const onDragOver = React.useCallback(\n    (event: React.DragEvent<HTMLDivElement>) => {\n      propsRef.current.onDragOver?.(event);\n\n      if (event.defaultPrevented) return;\n\n      event.preventDefault();\n      store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: true });\n    },\n    [store, propsRef],\n  );\n\n  const onDragEnter = React.useCallback(\n    (event: React.DragEvent<HTMLDivElement>) => {\n      propsRef.current.onDragEnter?.(event);\n\n      if (event.defaultPrevented) return;\n\n      event.preventDefault();\n      store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: true });\n    },\n    [store, propsRef],\n  );\n\n  const onDragLeave = React.useCallback(\n    (event: React.DragEvent<HTMLDivElement>) => {\n      propsRef.current.onDragLeave?.(event);\n\n      if (event.defaultPrevented) return;\n\n      const relatedTarget = event.relatedTarget;\n      if (\n        relatedTarget &&\n        relatedTarget instanceof Node &&\n        event.currentTarget.contains(relatedTarget)\n      ) {\n        return;\n      }\n\n      event.preventDefault();\n      store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: false });\n    },\n    [store, propsRef],\n  );\n\n  const onDrop = React.useCallback(\n    (event: React.DragEvent<HTMLDivElement>) => {\n      propsRef.current.onDrop?.(event);\n\n      if (event.defaultPrevented) return;\n\n      event.preventDefault();\n      store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: false });\n\n      const files = Array.from(event.dataTransfer.files);\n      const inputElement = context.inputRef.current;\n      if (!inputElement) return;\n\n      const dataTransfer = new DataTransfer();\n      for (const file of files) {\n        dataTransfer.items.add(file);\n      }\n\n      inputElement.files = dataTransfer.files;\n      inputElement.dispatchEvent(new Event(\"change\", { bubbles: true }));\n    },\n    [store, context.inputRef, propsRef],\n  );\n\n  const onPaste = React.useCallback(\n    (event: React.ClipboardEvent<HTMLDivElement>) => {\n      propsRef.current.onPaste?.(event);\n\n      if (event.defaultPrevented) return;\n\n      event.preventDefault();\n      store.dispatch({ type: \"SET_DRAG_OVER\", dragOver: false });\n\n      const items = event.clipboardData?.items;\n      if (!items) return;\n\n      const files: File[] = [];\n      for (let i = 0; i < items.length; i++) {\n        const item = items[i];\n        if (item?.kind === \"file\") {\n          const file = item.getAsFile();\n          if (file) {\n            files.push(file);\n          }\n        }\n      }\n\n      if (files.length === 0) return;\n\n      const inputElement = context.inputRef.current;\n      if (!inputElement) return;\n\n      const dataTransfer = new DataTransfer();\n      for (const file of files) {\n        dataTransfer.items.add(file);\n      }\n\n      inputElement.files = dataTransfer.files;\n      inputElement.dispatchEvent(new Event(\"change\", { bubbles: true }));\n    },\n    [store, context.inputRef, propsRef],\n  );\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      propsRef.current.onKeyDown?.(event);\n\n      if (\n        !event.defaultPrevented &&\n        (event.key === \"Enter\" || event.key === \" \")\n      ) {\n        event.preventDefault();\n        context.inputRef.current?.click();\n      }\n    },\n    [context.inputRef, propsRef],\n  );\n\n  const DropzonePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <DropzonePrimitive\n      role=\"region\"\n      id={context.dropzoneId}\n      aria-controls={`${context.inputId} ${context.listId}`}\n      aria-disabled={context.disabled}\n      aria-invalid={invalid}\n      data-disabled={context.disabled ? \"\" : undefined}\n      data-dragging={dragOver ? \"\" : undefined}\n      data-invalid={invalid ? \"\" : undefined}\n      data-slot=\"file-upload-dropzone\"\n      dir={context.dir}\n      tabIndex={context.disabled ? undefined : 0}\n      {...dropzoneProps}\n      className={cn(\n        \"relative flex select-none flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 outline-none transition-colors hover:bg-accent/30 focus-visible:border-ring/50 data-disabled:pointer-events-none data-dragging:border-primary/30 data-invalid:border-destructive data-dragging:bg-accent/30 data-invalid:ring-destructive/20\",\n        className,\n      )}\n      onClick={onClick}\n      onDragEnter={onDragEnter}\n      onDragLeave={onDragLeave}\n      onDragOver={onDragOver}\n      onDrop={onDrop}\n      onKeyDown={onKeyDown}\n      onPaste={onPaste}\n    />\n  );\n}\n\ninterface FileUploadTriggerProps extends React.ComponentProps<\"button\"> {\n  asChild?: boolean;\n}\n\nfunction FileUploadTrigger(props: FileUploadTriggerProps) {\n  const { asChild, onClick: onClickProp, ...triggerProps } = props;\n\n  const context = useFileUploadContext(TRIGGER_NAME);\n\n  const propsRef = useAsRef({\n    onClick: onClickProp,\n  });\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      propsRef.current.onClick?.(event);\n\n      if (event.defaultPrevented) return;\n\n      context.inputRef.current?.click();\n    },\n    [context.inputRef, propsRef],\n  );\n\n  const TriggerPrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <TriggerPrimitive\n      type=\"button\"\n      aria-controls={context.inputId}\n      data-disabled={context.disabled ? \"\" : undefined}\n      data-slot=\"file-upload-trigger\"\n      {...triggerProps}\n      disabled={context.disabled}\n      onClick={onClick}\n    />\n  );\n}\n\ninterface FileUploadListProps extends React.ComponentProps<\"div\"> {\n  orientation?: \"horizontal\" | \"vertical\";\n  asChild?: boolean;\n  forceMount?: boolean;\n}\n\nfunction FileUploadList(props: FileUploadListProps) {\n  const {\n    className,\n    orientation = \"vertical\",\n    asChild,\n    forceMount,\n    ...listProps\n  } = props;\n\n  const context = useFileUploadContext(LIST_NAME);\n  const fileCount = useStore((state) => state.files.size);\n  const shouldRender = forceMount || fileCount > 0;\n\n  if (!shouldRender) return null;\n\n  const ListPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ListPrimitive\n      role=\"list\"\n      id={context.listId}\n      aria-orientation={orientation}\n      data-orientation={orientation}\n      data-slot=\"file-upload-list\"\n      data-state={shouldRender ? \"active\" : \"inactive\"}\n      dir={context.dir}\n      {...listProps}\n      className={cn(\n        \"data-[state=inactive]:fade-out-0 data-[state=active]:fade-in-0 data-[state=inactive]:slide-out-to-top-2 data-[state=active]:slide-in-from-top-2 flex flex-col gap-2 data-[state=active]:animate-in data-[state=inactive]:animate-out\",\n        orientation === \"horizontal\" && \"flex-row overflow-x-auto p-1.5\",\n        className,\n      )}\n    />\n  );\n}\n\ninterface FileUploadItemContextValue {\n  id: string;\n  fileState: FileState | undefined;\n  nameId: string;\n  sizeId: string;\n  statusId: string;\n  messageId: string;\n}\n\nconst FileUploadItemContext =\n  React.createContext<FileUploadItemContextValue | null>(null);\n\nfunction useFileUploadItemContext(consumerName: string) {\n  const context = React.useContext(FileUploadItemContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n  }\n  return context;\n}\n\ninterface FileUploadItemProps extends React.ComponentProps<\"div\"> {\n  value: File;\n  asChild?: boolean;\n}\n\nfunction FileUploadItem(props: FileUploadItemProps) {\n  const { value, asChild, className, ...itemProps } = props;\n\n  const id = React.useId();\n  const statusId = `${id}-status`;\n  const nameId = `${id}-name`;\n  const sizeId = `${id}-size`;\n  const messageId = `${id}-message`;\n\n  const context = useFileUploadContext(ITEM_NAME);\n  const fileState = useStore((state) => state.files.get(value));\n  const fileCount = useStore((state) => state.files.size);\n  const fileIndex = useStore((state) => {\n    const files = Array.from(state.files.keys());\n    return files.indexOf(value) + 1;\n  });\n\n  const itemContext = React.useMemo(\n    () => ({\n      id,\n      fileState,\n      nameId,\n      sizeId,\n      statusId,\n      messageId,\n    }),\n    [id, fileState, statusId, nameId, sizeId, messageId],\n  );\n\n  if (!fileState) return null;\n\n  const statusText = fileState.error\n    ? `Error: ${fileState.error}`\n    : fileState.status === \"uploading\"\n      ? `Uploading: ${fileState.progress}% complete`\n      : fileState.status === \"success\"\n        ? \"Upload complete\"\n        : \"Ready to upload\";\n\n  const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <FileUploadItemContext.Provider value={itemContext}>\n      <ItemPrimitive\n        role=\"listitem\"\n        id={id}\n        aria-setsize={fileCount}\n        aria-posinset={fileIndex}\n        aria-describedby={`${nameId} ${sizeId} ${statusId} ${\n          fileState.error ? messageId : \"\"\n        }`}\n        aria-labelledby={nameId}\n        data-slot=\"file-upload-item\"\n        dir={context.dir}\n        {...itemProps}\n        className={cn(\n          \"relative flex items-center gap-2.5 rounded-md border p-3\",\n          className,\n        )}\n      >\n        {props.children}\n        <span id={statusId} className=\"sr-only\">\n          {statusText}\n        </span>\n      </ItemPrimitive>\n    </FileUploadItemContext.Provider>\n  );\n}\n\ninterface FileUploadItemPreviewProps extends React.ComponentProps<\"div\"> {\n  render?: (file: File, fallback: () => React.ReactNode) => React.ReactNode;\n  asChild?: boolean;\n}\n\nfunction FileUploadItemPreview(props: FileUploadItemPreviewProps) {\n  const { render, asChild, children, className, ...previewProps } = props;\n\n  const itemContext = useFileUploadItemContext(ITEM_PREVIEW_NAME);\n  const context = useFileUploadContext(ITEM_PREVIEW_NAME);\n\n  const getDefaultRender = React.useCallback(\n    (file: File) => {\n      if (itemContext.fileState?.file.type.startsWith(\"image/\")) {\n        let url = context.urlCache.get(file);\n        if (!url) {\n          url = URL.createObjectURL(file);\n          context.urlCache.set(file, url);\n        }\n\n        return (\n          // biome-ignore lint/performance/noImgElement: dynamic file URLs from user uploads don't work well with Next.js Image optimization\n          <img src={url} alt={file.name} className=\"size-full object-cover\" />\n        );\n      }\n\n      return getFileIcon(file);\n    },\n    [itemContext.fileState?.file.type, context.urlCache],\n  );\n\n  const onPreviewRender = React.useCallback(\n    (file: File) => {\n      if (render) {\n        return render(file, () => getDefaultRender(file));\n      }\n\n      return getDefaultRender(file);\n    },\n    [render, getDefaultRender],\n  );\n\n  if (!itemContext.fileState) return null;\n\n  const ItemPreviewPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ItemPreviewPrimitive\n      aria-labelledby={itemContext.nameId}\n      data-slot=\"file-upload-preview\"\n      {...previewProps}\n      className={cn(\n        \"relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded border bg-accent/50 [&>svg]:size-10\",\n        className,\n      )}\n    >\n      {onPreviewRender(itemContext.fileState.file)}\n      {children}\n    </ItemPreviewPrimitive>\n  );\n}\n\ninterface FileUploadItemMetadataProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n  size?: \"default\" | \"sm\";\n}\n\nfunction FileUploadItemMetadata(props: FileUploadItemMetadataProps) {\n  const {\n    asChild,\n    size = \"default\",\n    children,\n    className,\n    ...metadataProps\n  } = props;\n\n  const context = useFileUploadContext(ITEM_METADATA_NAME);\n  const itemContext = useFileUploadItemContext(ITEM_METADATA_NAME);\n\n  if (!itemContext.fileState) return null;\n\n  const ItemMetadataPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ItemMetadataPrimitive\n      data-slot=\"file-upload-metadata\"\n      dir={context.dir}\n      {...metadataProps}\n      className={cn(\"flex min-w-0 flex-1 flex-col\", className)}\n    >\n      {children ?? (\n        <>\n          <span\n            id={itemContext.nameId}\n            className={cn(\n              \"truncate font-medium text-sm\",\n              size === \"sm\" && \"font-normal text-[13px] leading-snug\",\n            )}\n          >\n            {itemContext.fileState.file.name}\n          </span>\n          <span\n            id={itemContext.sizeId}\n            className={cn(\n              \"truncate text-muted-foreground text-xs\",\n              size === \"sm\" && \"text-[11px] leading-snug\",\n            )}\n          >\n            {formatBytes(itemContext.fileState.file.size)}\n          </span>\n          {itemContext.fileState.error && (\n            <span\n              id={itemContext.messageId}\n              className=\"text-destructive text-xs\"\n            >\n              {itemContext.fileState.error}\n            </span>\n          )}\n        </>\n      )}\n    </ItemMetadataPrimitive>\n  );\n}\ninterface FileUploadItemProgressProps extends React.ComponentProps<\"div\"> {\n  variant?: \"linear\" | \"circular\" | \"fill\";\n  size?: number;\n  asChild?: boolean;\n  forceMount?: boolean;\n}\n\nfunction FileUploadItemProgress(props: FileUploadItemProgressProps) {\n  const {\n    variant = \"linear\",\n    size = 40,\n    asChild,\n    forceMount,\n    className,\n    ...progressProps\n  } = props;\n\n  const itemContext = useFileUploadItemContext(ITEM_PROGRESS_NAME);\n\n  if (!itemContext.fileState) return null;\n\n  const shouldRender = forceMount || itemContext.fileState.progress !== 100;\n\n  if (!shouldRender) return null;\n\n  const ItemProgressPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  switch (variant) {\n    case \"circular\": {\n      const circumference = 2 * Math.PI * ((size - 4) / 2);\n      const strokeDashoffset =\n        circumference - (itemContext.fileState.progress / 100) * circumference;\n\n      return (\n        <ItemProgressPrimitive\n          role=\"progressbar\"\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={itemContext.fileState.progress}\n          aria-valuetext={`${itemContext.fileState.progress}%`}\n          aria-labelledby={itemContext.nameId}\n          data-slot=\"file-upload-progress\"\n          {...progressProps}\n          className={cn(\n            \"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\",\n            className,\n          )}\n        >\n          <svg\n            className=\"-rotate-90 transform\"\n            width={size}\n            height={size}\n            viewBox={`0 0 ${size} ${size}`}\n            fill=\"none\"\n            stroke=\"currentColor\"\n          >\n            <circle\n              className=\"text-primary/20\"\n              strokeWidth=\"2\"\n              cx={size / 2}\n              cy={size / 2}\n              r={(size - 4) / 2}\n            />\n            <circle\n              className=\"text-primary transition-[stroke-dashoffset] duration-300 ease-linear\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeDasharray={circumference}\n              strokeDashoffset={strokeDashoffset}\n              cx={size / 2}\n              cy={size / 2}\n              r={(size - 4) / 2}\n            />\n          </svg>\n        </ItemProgressPrimitive>\n      );\n    }\n\n    case \"fill\": {\n      const progressPercentage = itemContext.fileState.progress;\n      const topInset = 100 - progressPercentage;\n\n      return (\n        <ItemProgressPrimitive\n          role=\"progressbar\"\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={progressPercentage}\n          aria-valuetext={`${progressPercentage}%`}\n          aria-labelledby={itemContext.nameId}\n          data-slot=\"file-upload-progress\"\n          {...progressProps}\n          className={cn(\n            \"absolute inset-0 bg-primary/50 transition-[clip-path] duration-300 ease-linear\",\n            className,\n          )}\n          style={{\n            clipPath: `inset(${topInset}% 0% 0% 0%)`,\n          }}\n        />\n      );\n    }\n\n    default:\n      return (\n        <ItemProgressPrimitive\n          role=\"progressbar\"\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={itemContext.fileState.progress}\n          aria-valuetext={`${itemContext.fileState.progress}%`}\n          aria-labelledby={itemContext.nameId}\n          data-slot=\"file-upload-progress\"\n          {...progressProps}\n          className={cn(\n            \"relative h-1.5 w-full overflow-hidden rounded-full bg-primary/20\",\n            className,\n          )}\n        >\n          <div\n            className=\"h-full w-full flex-1 bg-primary transition-transform duration-300 ease-linear\"\n            style={{\n              transform: `translateX(-${100 - itemContext.fileState.progress}%)`,\n            }}\n          />\n        </ItemProgressPrimitive>\n      );\n  }\n}\n\ninterface FileUploadItemDeleteProps extends React.ComponentProps<\"button\"> {\n  asChild?: boolean;\n}\n\nfunction FileUploadItemDelete(props: FileUploadItemDeleteProps) {\n  const { asChild, onClick: onClickProp, ...deleteProps } = props;\n\n  const store = useStoreContext(ITEM_DELETE_NAME);\n  const itemContext = useFileUploadItemContext(ITEM_DELETE_NAME);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      onClickProp?.(event);\n\n      if (!itemContext.fileState || event.defaultPrevented) return;\n\n      store.dispatch({\n        type: \"REMOVE_FILE\",\n        file: itemContext.fileState.file,\n      });\n    },\n    [store, itemContext.fileState, onClickProp],\n  );\n\n  if (!itemContext.fileState) return null;\n\n  const ItemDeletePrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <ItemDeletePrimitive\n      type=\"button\"\n      aria-controls={itemContext.id}\n      aria-describedby={itemContext.nameId}\n      data-slot=\"file-upload-item-delete\"\n      {...deleteProps}\n      onClick={onClick}\n    />\n  );\n}\n\ninterface FileUploadClearProps extends React.ComponentProps<\"button\"> {\n  forceMount?: boolean;\n  asChild?: boolean;\n}\n\nfunction FileUploadClear(props: FileUploadClearProps) {\n  const {\n    asChild,\n    forceMount,\n    disabled,\n    onClick: onClickProp,\n    ...clearProps\n  } = props;\n\n  const context = useFileUploadContext(CLEAR_NAME);\n  const store = useStoreContext(CLEAR_NAME);\n  const fileCount = useStore((state) => state.files.size);\n\n  const isDisabled = disabled || context.disabled;\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      onClickProp?.(event);\n\n      if (event.defaultPrevented) return;\n\n      store.dispatch({ type: \"CLEAR\" });\n    },\n    [store, onClickProp],\n  );\n\n  const shouldRender = forceMount || fileCount > 0;\n\n  if (!shouldRender) return null;\n\n  const ClearPrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <ClearPrimitive\n      type=\"button\"\n      aria-controls={context.listId}\n      data-slot=\"file-upload-clear\"\n      data-disabled={isDisabled ? \"\" : undefined}\n      {...clearProps}\n      disabled={isDisabled}\n      onClick={onClick}\n    />\n  );\n}\n\nexport {\n  FileUpload,\n  FileUploadClear,\n  FileUploadDropzone,\n  FileUploadItem,\n  FileUploadItemDelete,\n  FileUploadItemMetadata,\n  FileUploadItemPreview,\n  FileUploadItemProgress,\n  FileUploadList,\n  type FileUploadProps,\n  FileUploadTrigger,\n  useStore as useFileUpload,\n};\n",
      "target": ""
    }
  ]
}