{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropzone",
  "title": "Dropzone",
  "description": "A headless file-intake primitive with drag/drop, file input wiring, validation, controlled state, and accessibility prop getters.",
  "dependencies": [
    "@radix-ui/react-slot@^1.1.1"
  ],
  "registryDependencies": [
    "@retab/use-keyed-mount-effect"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/dropzone.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport {\n  formatDropzoneAccept,\n  parseDropzoneAccept,\n  validateDropzoneFiles,\n  type DropzoneAcceptRule,\n  type DropzoneFileRejection,\n  type DropzoneIntake,\n} from \"@/components/ui/dropzone-core\";\n\nexport {\n  formatDropzoneAccept,\n  matchesDropzoneAccept,\n  parseDropzoneAccept,\n  validateDropzoneFile,\n  validateDropzoneFiles,\n  type DropzoneAcceptRule,\n  type DropzoneFileRejection,\n  type DropzoneIntake,\n} from \"@/components/ui/dropzone-core\";\n\nexport type DropzoneFileItem = {\n  id: string;\n  file: File;\n};\n\ntype MaybePromise<T> = T | Promise<T>;\n\nexport type DropzoneFilesValidatorContext = {\n  acceptedFiles: File[];\n  currentCount: number;\n  currentFiles: DropzoneFileItem[];\n  fileRejections: DropzoneFileRejection[];\n};\n\nexport type DropzoneFilesValidatorResult =\n  | DropzoneFileRejection[]\n  | DropzoneIntake\n  | null\n  | undefined\n  | void;\n\nexport type DropzoneIntakeSource = \"drop\" | \"input\" | (string & {});\n\nexport type DropzoneIntakeDetails = {\n  source: DropzoneIntakeSource;\n};\n\nexport type DropzoneOpenFileDialogOptions = {\n  source?: DropzoneIntakeSource;\n};\n\ntype DropzoneDataAttributes = {\n  [key: `data-${string}`]: string | undefined;\n};\n\ntype DropzoneRootGetterProps<T extends HTMLElement> = React.HTMLAttributes<T> &\n  Partial<DropzoneDataAttributes>;\n\ntype DropzoneInputGetterProps = React.ComponentPropsWithRef<\"input\"> &\n  Partial<DropzoneDataAttributes>;\n\ntype DropzoneTriggerGetterProps<T extends HTMLElement> =\n  React.HTMLAttributes<T> &\n    Partial<DropzoneDataAttributes> & {\n      /** The trigger is a real `<button>`; suppress the ARIA-button polyfill. */\n      native?: boolean;\n      source?: DropzoneIntakeSource;\n    };\n\nexport type UseDropzoneProps = {\n  accept?: string | DropzoneAcceptRule[];\n  inputAccept?: string | DropzoneAcceptRule[];\n  currentFileCount?: number;\n  disabled?: boolean;\n  dragScope?: \"root\" | \"document\";\n  files?: DropzoneFileItem[];\n  defaultFiles?: DropzoneFileItem[];\n  maxFiles?: number;\n  maxSize?: number;\n  multiple?: boolean;\n  onFilesChange?: (files: DropzoneFileItem[]) => void;\n  onIntake?: (intake: DropzoneIntake, details: DropzoneIntakeDetails) => void;\n  storeFiles?: boolean;\n  validateFiles?: (\n    files: File[],\n    context: DropzoneFilesValidatorContext,\n  ) => MaybePromise<DropzoneFilesValidatorResult>;\n};\n\nexport type UseDropzoneReturn = {\n  files: DropzoneFileItem[];\n  lastIntake: DropzoneIntake;\n  lastIntakeDetails: DropzoneIntakeDetails | null;\n  isDragging: boolean;\n  isDisabled: boolean;\n  isValidating: boolean;\n  clearFiles: () => void;\n  openFileDialog: (options?: DropzoneOpenFileDialogOptions) => void;\n  removeFile: (fileId: string) => void;\n  reset: () => void;\n  resetIntake: () => void;\n  getRootProps: <T extends HTMLElement>(\n    props?: DropzoneRootGetterProps<T>,\n  ) => DropzoneRootGetterProps<T>;\n  getInputProps: (props?: DropzoneInputGetterProps) => DropzoneInputGetterProps;\n  getTriggerProps: <T extends HTMLElement>(\n    props?: DropzoneTriggerGetterProps<T>,\n  ) => DropzoneTriggerGetterProps<T>;\n};\n\ntype DropzoneContextValue = UseDropzoneReturn;\n\nconst DropzoneContext = React.createContext<DropzoneContextValue | null>(null);\n\nexport type DropzoneProviderProps = UseDropzoneProps & {\n  children: React.ReactNode;\n};\n\nexport function DropzoneProvider({\n  children,\n  ...dropzoneProps\n}: DropzoneProviderProps) {\n  const dropzone = useDropzone(dropzoneProps);\n\n  return (\n    <DropzoneContext.Provider value={dropzone}>\n      {children}\n    </DropzoneContext.Provider>\n  );\n}\n\nexport function useDropzoneContext(\n  consumerName = \"useDropzoneContext\",\n): DropzoneContextValue {\n  const dropzone = React.useContext(DropzoneContext);\n  if (!dropzone) {\n    throw new Error(`${consumerName} must be used within DropzoneProvider.`);\n  }\n  return dropzone;\n}\n\nexport type DropzoneStateProps = {\n  children: (dropzone: DropzoneContextValue) => React.ReactNode;\n};\n\nexport function DropzoneState({ children }: DropzoneStateProps) {\n  const dropzone = useDropzoneContext(\"DropzoneState\");\n  return <>{children(dropzone)}</>;\n}\n\nexport type DropzoneRootProps = React.HTMLAttributes<HTMLElement> &\n  Partial<DropzoneDataAttributes> & {\n    asChild?: boolean;\n  };\n\nexport const DropzoneRoot = React.forwardRef<HTMLElement, DropzoneRootProps>(\n  function DropzoneRoot({ asChild = false, ...props }, ref) {\n    const dropzone = useDropzoneContext(\"DropzoneRoot\");\n    const Comp = asChild ? Slot : \"div\";\n\n    return (\n      <Comp\n        {...dropzone.getRootProps(props)}\n        ref={ref as React.Ref<HTMLDivElement>}\n      />\n    );\n  },\n);\n\nexport type DropzoneInputProps = React.ComponentPropsWithoutRef<\"input\"> &\n  Partial<DropzoneDataAttributes>;\n\nexport const DropzoneInput = React.forwardRef<\n  HTMLInputElement,\n  DropzoneInputProps\n>(function DropzoneInput(props, ref) {\n  const dropzone = useDropzoneContext(\"DropzoneInput\");\n\n  return <input {...dropzone.getInputProps({ ...props, ref })} />;\n});\n\nexport type DropzoneTriggerProps = React.ComponentPropsWithoutRef<\"button\"> &\n  Partial<DropzoneDataAttributes> & {\n    asChild?: boolean;\n    native?: boolean;\n    source?: DropzoneIntakeSource;\n  };\n\nexport const DropzoneTrigger = React.forwardRef<\n  HTMLElement,\n  DropzoneTriggerProps\n>(function DropzoneTrigger({ asChild = false, native, ...props }, ref) {\n  const dropzone = useDropzoneContext(\"DropzoneTrigger\");\n  const Comp = asChild ? Slot : \"button\";\n  const triggerProps = dropzone.getTriggerProps({\n    ...props,\n    native: native ?? !asChild,\n  });\n\n  return <Comp {...triggerProps} ref={ref as React.Ref<HTMLButtonElement>} />;\n});\n\nconst EMPTY_INTAKE: DropzoneIntake = {\n  acceptedFiles: [],\n  fileRejections: [],\n};\nconst EMPTY_FILE_ITEMS: DropzoneFileItem[] = [];\n\nexport function useDropzone({\n  accept,\n  inputAccept: nativeInputAccept,\n  currentFileCount,\n  defaultFiles = [],\n  disabled = false,\n  dragScope = \"root\",\n  files,\n  maxFiles,\n  maxSize,\n  multiple = true,\n  onFilesChange,\n  onIntake,\n  storeFiles = true,\n  validateFiles,\n}: UseDropzoneProps = {}): UseDropzoneReturn {\n  const inputRef = React.useRef<HTMLInputElement | null>(null);\n  const activeDialogSourceRef = React.useRef<DropzoneIntakeSource | undefined>(\n    undefined,\n  );\n  const dragDepthRef = React.useRef(0);\n  const intakeRequestRef = React.useRef(0);\n  const isDisabledRef = React.useRef(disabled);\n  const shouldStoreFiles = storeFiles;\n  const isControlled = shouldStoreFiles && files !== undefined;\n  const acceptRules = React.useMemo<DropzoneAcceptRule[]>(\n    () => (Array.isArray(accept) ? accept : parseDropzoneAccept(accept)),\n    [accept],\n  );\n  const inputAccept = React.useMemo(\n    () => formatDropzoneAccept(nativeInputAccept ?? accept),\n    [accept, nativeInputAccept],\n  );\n  const [uncontrolledItems, setUncontrolledItems] =\n    React.useState<DropzoneFileItem[]>(defaultFiles);\n  const [lastIntake, setLastIntake] =\n    React.useState<DropzoneIntake>(EMPTY_INTAKE);\n  const [lastIntakeDetails, setLastIntakeDetails] =\n    React.useState<DropzoneIntakeDetails | null>(null);\n  const [rawIsDragging, setIsDragging] = React.useState(false);\n  const [isValidating, setIsValidating] = React.useState(false);\n  const currentItems = shouldStoreFiles\n    ? (files ?? uncontrolledItems)\n    : EMPTY_FILE_ITEMS;\n  const isDragging = disabled ? false : rawIsDragging;\n\n  // itemsRef.current is the latest committed items. The effect mirrors the\n  // source of truth into it after every render; internal commits update it\n  // eagerly so consecutive same-tick intakes — and the pre-validation count in\n  // commitFiles — read the value they just produced. A controlled parent owns\n  // the truth: the eager write is optimistic and the effect reconciles it on\n  // the parent's next render.\n  const itemsRef = React.useRef(currentItems);\n  itemsRef.current = currentItems;\n  isDisabledRef.current = disabled;\n\n  const invalidatePendingIntake = React.useCallback(\n    (clearValidationState = true) => {\n      intakeRequestRef.current += 1;\n      activeDialogSourceRef.current = undefined;\n      if (clearValidationState) setIsValidating(false);\n    },\n    [],\n  );\n\n  const commitFileTransition = React.useCallback(\n    (transition: (items: DropzoneFileItem[]) => DropzoneFileItem[]) => {\n      if (!shouldStoreFiles) return;\n      const nextItems = transition(itemsRef.current);\n      itemsRef.current = nextItems;\n      if (!isControlled) setUncontrolledItems(nextItems);\n      onFilesChange?.(nextItems);\n    },\n    [isControlled, onFilesChange, shouldStoreFiles],\n  );\n\n  const resetDragState = React.useCallback(() => {\n    dragDepthRef.current = 0;\n    setIsDragging(false);\n  }, []);\n\n  const resetIntake = React.useCallback(() => {\n    setLastIntake(EMPTY_INTAKE);\n    setLastIntakeDetails(null);\n  }, []);\n\n  const clearFiles = React.useCallback(() => {\n    if (disabled || !shouldStoreFiles) return;\n    commitFileTransition(() => []);\n  }, [commitFileTransition, disabled, shouldStoreFiles]);\n\n  const reset = React.useCallback(() => {\n    if (disabled) return;\n    resetDragState();\n    resetIntake();\n    if (shouldStoreFiles) commitFileTransition(() => []);\n  }, [\n    commitFileTransition,\n    disabled,\n    resetDragState,\n    resetIntake,\n    shouldStoreFiles,\n  ]);\n\n  const removeFile = React.useCallback(\n    (fileId: string) => {\n      if (disabled || !shouldStoreFiles) return;\n      commitFileTransition((previousItems) =>\n        previousItems.filter((item) => item.id !== fileId),\n      );\n    },\n    [commitFileTransition, disabled, shouldStoreFiles],\n  );\n\n  const commitFiles = React.useCallback(\n    async (nextFiles: FileList | File[], details: DropzoneIntakeDetails) => {\n      if (disabled) return;\n\n      const requestId = intakeRequestRef.current + 1;\n      intakeRequestRef.current = requestId;\n      const incomingFiles = Array.from(nextFiles);\n      const baseItems = multiple ? itemsRef.current : [];\n      const effectiveCurrentCount = currentFileCount ?? baseItems.length;\n      const effectiveMaxFiles = multiple\n        ? maxFiles\n        : Math.min(maxFiles ?? 1, 1);\n      const intake = validateDropzoneFiles(incomingFiles, {\n        accept: acceptRules,\n        maxSize,\n      });\n      let validatedIntake = intake;\n      if (validateFiles) {\n        setIsValidating(true);\n        try {\n          validatedIntake = await resolveDropzoneValidation({\n            currentCount: effectiveCurrentCount,\n            currentFiles: baseItems,\n            intake,\n            validateFiles,\n          });\n        } catch (error) {\n          validatedIntake = rejectAcceptedFilesForValidationError(\n            intake,\n            error,\n          );\n        } finally {\n          if (intakeRequestRef.current === requestId) {\n            setIsValidating(false);\n          }\n        }\n      }\n\n      if (intakeRequestRef.current !== requestId || isDisabledRef.current) {\n        return;\n      }\n\n      const finalIntake = applyDropzoneMaxFiles(validatedIntake, {\n        currentCount: effectiveCurrentCount,\n        maxFiles: effectiveMaxFiles,\n      });\n      setLastIntake(finalIntake);\n      setLastIntakeDetails(details);\n      onIntake?.(finalIntake, details);\n      if (!shouldStoreFiles || finalIntake.acceptedFiles.length === 0) {\n        return;\n      }\n\n      const acceptedItems = finalIntake.acceptedFiles.map((file) => ({\n        id: createDropzoneFileId(file),\n        file,\n      }));\n      commitFileTransition((previousItems) =>\n        createNextDropzoneItems({\n          acceptedItems,\n          multiple,\n          previousItems,\n        }),\n      );\n    },\n    [\n      acceptRules,\n      commitFileTransition,\n      currentFileCount,\n      disabled,\n      maxFiles,\n      maxSize,\n      multiple,\n      onIntake,\n      shouldStoreFiles,\n      validateFiles,\n    ],\n  );\n  const commitFilesRef = React.useRef(commitFiles);\n  commitFilesRef.current = commitFiles;\n\n  const openFileDialog = React.useCallback(\n    (options: DropzoneOpenFileDialogOptions = {}) => {\n      if (disabled || !inputRef.current) return;\n      activeDialogSourceRef.current = options.source;\n      inputRef.current.click();\n    },\n    [disabled],\n  );\n\n  useKeyedMountEffect(\"dropzone-lifecycle\", () => {\n    return () => {\n      invalidatePendingIntake(false);\n    };\n  });\n\n  useKeyedMountEffect(disabled ? \"disabled\" : null, () => {\n    if (disabled) {\n      invalidatePendingIntake();\n      resetDragState();\n    }\n  });\n\n  useKeyedMountEffect(\n    dragScope === \"document\" && !disabled ? \"document-drag-listeners\" : null,\n    () => {\n      // Document-level drag listeners are imperative browser wiring for overlays outside the root.\n      const handleDocumentDragEnter = (event: DragEvent) => {\n        if (!hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        dragDepthRef.current += 1;\n        setIsDragging(true);\n      };\n      const handleDocumentDragLeave = (event: DragEvent) => {\n        if (!hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);\n        if (dragDepthRef.current === 0) setIsDragging(false);\n      };\n      const handleDocumentDragOver = (event: DragEvent) => {\n        if (!hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        event.dataTransfer!.dropEffect = \"copy\";\n      };\n      const handleDocumentDrop = (event: DragEvent) => {\n        if (!hasDraggedFiles(event.dataTransfer)) {\n          resetDragState();\n          return;\n        }\n        event.preventDefault();\n        resetDragState();\n        if (event.dataTransfer?.files.length) {\n          void commitFilesRef.current(event.dataTransfer.files, {\n            source: \"drop\",\n          });\n        }\n      };\n      const handleDocumentDragEnd = () => {\n        resetDragState();\n      };\n\n      document.addEventListener(\"dragenter\", handleDocumentDragEnter, true);\n      document.addEventListener(\"dragleave\", handleDocumentDragLeave, true);\n      document.addEventListener(\"dragover\", handleDocumentDragOver, true);\n      document.addEventListener(\"drop\", handleDocumentDrop, true);\n      document.addEventListener(\"dragend\", handleDocumentDragEnd, true);\n\n      return () => {\n        document.removeEventListener(\n          \"dragenter\",\n          handleDocumentDragEnter,\n          true,\n        );\n        document.removeEventListener(\n          \"dragleave\",\n          handleDocumentDragLeave,\n          true,\n        );\n        document.removeEventListener(\"dragover\", handleDocumentDragOver, true);\n        document.removeEventListener(\"drop\", handleDocumentDrop, true);\n        document.removeEventListener(\"dragend\", handleDocumentDragEnd, true);\n      };\n    },\n  );\n\n  const getRootProps = React.useCallback(\n    <T extends HTMLElement>(\n      props: DropzoneRootGetterProps<T> = {},\n    ): DropzoneRootGetterProps<T> => ({\n      ...props,\n      \"aria-disabled\": disabled || props[\"aria-disabled\"] || undefined,\n      \"data-dragging\": isDragging ? \"\" : undefined,\n      \"data-slot\": props[\"data-slot\"] ?? \"dropzone\",\n      onDragEnter: composeEventHandlers(props.onDragEnter, (event) => {\n        if (disabled || !hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        if (dragScope === \"root\") {\n          dragDepthRef.current += 1;\n          setIsDragging(true);\n        }\n      }),\n      onDragLeave: composeEventHandlers(props.onDragLeave, (event) => {\n        if (disabled || !hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        if (dragScope === \"root\") {\n          dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);\n          if (dragDepthRef.current === 0) setIsDragging(false);\n        }\n      }),\n      onDragOver: composeEventHandlers(props.onDragOver, (event) => {\n        if (disabled || !hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        event.dataTransfer.dropEffect = \"copy\";\n      }),\n      onDrop: composeEventHandlers(props.onDrop, (event) => {\n        if (disabled || !hasDraggedFiles(event.dataTransfer)) return;\n        event.preventDefault();\n        resetDragState();\n        if (event.dataTransfer.files.length > 0) {\n          void commitFiles(event.dataTransfer.files, { source: \"drop\" });\n        }\n      }),\n    }),\n    [commitFiles, disabled, dragScope, isDragging, resetDragState],\n  );\n\n  const getInputProps = React.useCallback(\n    (props: DropzoneInputGetterProps = {}): DropzoneInputGetterProps => ({\n      ...props,\n      accept: inputAccept,\n      disabled,\n      multiple,\n      ref: composeRefs(inputRef, props.ref),\n      type: \"file\",\n      \"data-slot\": props[\"data-slot\"] ?? \"dropzone-input\",\n      onChange: composeEventHandlers(props.onChange, (event) => {\n        if (disabled) return;\n        if (event.currentTarget.files) {\n          const source = activeDialogSourceRef.current ?? \"input\";\n          activeDialogSourceRef.current = undefined;\n          void commitFiles(event.currentTarget.files, { source });\n          event.currentTarget.value = \"\";\n        }\n      }),\n    }),\n    [commitFiles, disabled, inputAccept, multiple],\n  );\n\n  const getTriggerProps = React.useCallback(\n    <T extends HTMLElement>({\n      native = false,\n      source,\n      ...props\n    }: DropzoneTriggerGetterProps<T> = {}): DropzoneTriggerGetterProps<T> => ({\n      ...props,\n      \"data-slot\": props[\"data-slot\"] ?? \"dropzone-trigger\",\n      onClick: composeEventHandlers(props.onClick, () => {\n        openFileDialog({ source });\n      }),\n      ...(native\n        ? // Native button: the platform owns role, focus, and keyboard\n          // activation; only the disabled attribute and button type are ours.\n          { disabled, type: \"button\" as const }\n        : // Anything else: polyfill button semantics onto the element.\n          {\n            role: props.role ?? \"button\",\n            tabIndex: disabled ? -1 : (props.tabIndex ?? 0),\n            \"aria-disabled\": disabled || props[\"aria-disabled\"] || undefined,\n            onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {\n              if (event.key === \"Enter\" || event.key === \" \") {\n                event.preventDefault();\n                openFileDialog({ source });\n              }\n            }),\n          }),\n    }),\n    [disabled, openFileDialog],\n  );\n\n  return React.useMemo(\n    () => ({\n      files: currentItems,\n      lastIntake,\n      lastIntakeDetails,\n      isDragging,\n      isDisabled: disabled,\n      isValidating,\n      clearFiles,\n      openFileDialog,\n      removeFile,\n      reset,\n      resetIntake,\n      getRootProps,\n      getInputProps,\n      getTriggerProps,\n    }),\n    [\n      clearFiles,\n      currentItems,\n      disabled,\n      getInputProps,\n      getRootProps,\n      getTriggerProps,\n      isDragging,\n      isValidating,\n      lastIntake,\n      lastIntakeDetails,\n      openFileDialog,\n      removeFile,\n      reset,\n      resetIntake,\n    ],\n  );\n}\n\nasync function resolveDropzoneValidation({\n  currentCount,\n  currentFiles,\n  intake,\n  validateFiles,\n}: {\n  currentCount: number;\n  currentFiles: DropzoneFileItem[];\n  intake: DropzoneIntake;\n  validateFiles: NonNullable<UseDropzoneProps[\"validateFiles\"]>;\n}): Promise<DropzoneIntake> {\n  if (intake.acceptedFiles.length === 0) return intake;\n\n  const result = await validateFiles(intake.acceptedFiles, {\n    acceptedFiles: intake.acceptedFiles,\n    currentCount,\n    currentFiles,\n    fileRejections: intake.fileRejections,\n  });\n  if (!result) return intake;\n\n  if (Array.isArray(result)) {\n    return appendDropzoneRejections(intake, result);\n  }\n\n  return {\n    acceptedFiles: result.acceptedFiles,\n    fileRejections: [...intake.fileRejections, ...result.fileRejections],\n  };\n}\n\nfunction applyDropzoneMaxFiles(\n  intake: DropzoneIntake,\n  {\n    currentCount,\n    maxFiles,\n  }: {\n    currentCount: number;\n    maxFiles: number | undefined;\n  },\n): DropzoneIntake {\n  if (maxFiles === undefined) return intake;\n\n  const availableSlots = Math.max(0, maxFiles - currentCount);\n  if (intake.acceptedFiles.length <= availableSlots) return intake;\n\n  const acceptedFiles = intake.acceptedFiles.slice(0, availableSlots);\n  const fileRejections = [\n    ...intake.fileRejections,\n    ...intake.acceptedFiles.slice(availableSlots).map((file) => ({\n      file,\n      reason: \"too-many-files\" as const,\n      maxFiles,\n    })),\n  ];\n\n  return { acceptedFiles, fileRejections };\n}\n\nfunction appendDropzoneRejections(\n  intake: DropzoneIntake,\n  rejections: DropzoneFileRejection[],\n): DropzoneIntake {\n  if (rejections.length === 0) return intake;\n  const rejectedFiles = new Set(rejections.map((rejection) => rejection.file));\n\n  return {\n    acceptedFiles: intake.acceptedFiles.filter(\n      (file) => !rejectedFiles.has(file),\n    ),\n    fileRejections: [...intake.fileRejections, ...rejections],\n  };\n}\n\nfunction rejectAcceptedFilesForValidationError(\n  intake: DropzoneIntake,\n  error: unknown,\n): DropzoneIntake {\n  return {\n    acceptedFiles: [],\n    fileRejections: [\n      ...intake.fileRejections,\n      ...intake.acceptedFiles.map((file) => ({\n        file,\n        reason: \"custom\" as const,\n        code: \"validation-error\",\n        details:\n          error instanceof Error\n            ? { message: error.message, name: error.name }\n            : error,\n      })),\n    ],\n  };\n}\n\nfunction createNextDropzoneItems({\n  acceptedItems,\n  multiple,\n  previousItems,\n}: {\n  acceptedItems: DropzoneFileItem[];\n  multiple: boolean;\n  previousItems: DropzoneFileItem[];\n}) {\n  return multiple ? [...previousItems, ...acceptedItems] : acceptedItems;\n}\n\nfunction createDropzoneFileId(file: File): string {\n  const uniqueId =\n    typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n      ? crypto.randomUUID()\n      : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n\n  return `${file.name}-${file.size}-${file.lastModified}-${uniqueId}`;\n}\n\nfunction hasDraggedFiles(\n  dataTransfer:\n    | Pick<DataTransfer, \"items\" | \"types\" | \"files\">\n    | null\n    | undefined,\n): boolean {\n  if (!dataTransfer) return false;\n  if (dataTransfer.items?.length) {\n    return Array.from(dataTransfer.items).some((item) => item.kind === \"file\");\n  }\n\n  return (\n    Array.from(dataTransfer.types ?? []).includes(\"Files\") ||\n    (dataTransfer.files?.length ?? 0) > 0\n  );\n}\n\nfunction composeEventHandlers<Event extends { defaultPrevented: boolean }>(\n  externalHandler: ((event: Event) => void) | undefined,\n  internalHandler: (event: Event) => void,\n) {\n  return (event: Event) => {\n    externalHandler?.(event);\n    if (!event.defaultPrevented) internalHandler(event);\n  };\n}\n\nfunction composeRefs<T>(\n  internalRef: React.MutableRefObject<T | null>,\n  externalRef: React.Ref<T> | undefined,\n) {\n  return (node: T | null) => {\n    internalRef.current = node;\n    if (!externalRef) return;\n    if (typeof externalRef === \"function\") {\n      externalRef(node);\n      return;\n    }\n    externalRef.current = node;\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/dropzone.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/dropzone-core.ts",
      "content": "export type DropzoneAcceptRule =\n  | { type: \"any\"; value: \"*/*\" }\n  | { type: \"extension\"; value: string }\n  | { type: \"mime\"; value: string }\n  | { type: \"mime-prefix\"; value: string };\n\nexport type DropzoneFileRejection =\n  | {\n      file: File;\n      reason: \"file-invalid-type\";\n      acceptRules: DropzoneAcceptRule[];\n    }\n  | {\n      file: File;\n      reason: \"file-too-large\";\n      maxSize: number;\n    }\n  | {\n      file: File;\n      reason: \"too-many-files\";\n      maxFiles: number;\n    }\n  | {\n      file: File;\n      reason: \"custom\";\n      code: string;\n      details?: unknown;\n    };\n\nexport type DropzoneIntake = {\n  acceptedFiles: File[];\n  fileRejections: DropzoneFileRejection[];\n};\n\nexport function parseDropzoneAccept(accept?: string): DropzoneAcceptRule[] {\n  if (!accept) return [];\n\n  return accept\n    .split(\",\")\n    .map((rawToken) => rawToken.trim().toLowerCase())\n    .filter(Boolean)\n    .map((token): DropzoneAcceptRule => {\n      if (token === \"*/*\") {\n        return { type: \"any\", value: \"*/*\" };\n      }\n      if (token.startsWith(\".\")) {\n        return { type: \"extension\", value: token };\n      }\n      if (token.endsWith(\"/*\")) {\n        return { type: \"mime-prefix\", value: token.slice(0, -1) };\n      }\n      return { type: \"mime\", value: token };\n    });\n}\n\nexport function formatDropzoneAccept(\n  accept?: string | DropzoneAcceptRule[],\n): string | undefined {\n  if (!Array.isArray(accept)) return accept;\n\n  return accept\n    .map((rule) => {\n      if (rule.type === \"any\") return rule.value;\n      if (rule.type === \"mime-prefix\") return `${rule.value}*`;\n      return rule.value;\n    })\n    .join(\",\");\n}\n\nexport function matchesDropzoneAccept(\n  file: File,\n  accept?: string | DropzoneAcceptRule[],\n): boolean {\n  const acceptRules = Array.isArray(accept)\n    ? accept\n    : parseDropzoneAccept(accept);\n  if (acceptRules.length === 0) return true;\n\n  const fileName = file.name.toLowerCase();\n  const fileType = file.type.toLowerCase();\n\n  return acceptRules.some((rule) => {\n    if (rule.type === \"any\") return true;\n    if (rule.type === \"extension\") return fileName.endsWith(rule.value);\n    if (rule.type === \"mime-prefix\") return fileType.startsWith(rule.value);\n    return fileType === rule.value;\n  });\n}\n\nexport function validateDropzoneFile(\n  file: File,\n  {\n    accept,\n    maxSize,\n  }: {\n    accept?: string | DropzoneAcceptRule[];\n    maxSize?: number;\n  },\n): DropzoneFileRejection | null {\n  if (!matchesDropzoneAccept(file, accept)) {\n    return {\n      file,\n      reason: \"file-invalid-type\",\n      acceptRules: Array.isArray(accept) ? accept : parseDropzoneAccept(accept),\n    };\n  }\n\n  if (maxSize !== undefined && file.size > maxSize) {\n    return {\n      file,\n      reason: \"file-too-large\",\n      maxSize,\n    };\n  }\n\n  return null;\n}\n\nexport function validateDropzoneFiles(\n  incomingFiles: File[],\n  {\n    accept,\n    currentCount = 0,\n    maxFiles,\n    maxSize,\n  }: {\n    accept?: string | DropzoneAcceptRule[];\n    currentCount?: number;\n    maxFiles?: number;\n    maxSize?: number;\n  },\n): DropzoneIntake {\n  const acceptedFiles: File[] = [];\n  const fileRejections: DropzoneFileRejection[] = [];\n  const availableSlots =\n    maxFiles === undefined ? Number.POSITIVE_INFINITY : maxFiles - currentCount;\n\n  for (const file of incomingFiles) {\n    const fileRejection = validateDropzoneFile(file, { accept, maxSize });\n    if (fileRejection) {\n      fileRejections.push(fileRejection);\n      continue;\n    }\n\n    if (acceptedFiles.length >= availableSlots) {\n      fileRejections.push({\n        file,\n        reason: \"too-many-files\",\n        maxFiles: maxFiles ?? 0,\n      });\n      continue;\n    }\n\n    acceptedFiles.push(file);\n  }\n\n  return { acceptedFiles, fileRejections };\n}\n",
      "type": "registry:ui",
      "target": "@ui/dropzone-core.ts"
    }
  ],
  "type": "registry:ui"
}