{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-file-upload",
  "title": "Chat File Upload",
  "description": "Files from the picker or dropped on the window, with a layer that shows while they are dragged in.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/chat-file-upload.tsx",
      "content": "// Adapted from prompt-kit (MIT, Copyright (c) 2025 Julien Thibeaut).\n'use client';\n\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type MotionProps,\n  type Transition,\n} from 'motion/react';\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useEffectEvent,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { cn } from '@/registry/ndeko/lib/utils';\n\ntype FileUploadContextValue = {\n  isDragging: boolean;\n  openPicker: () => void;\n  multiple: boolean;\n  disabled: boolean;\n};\n\nconst FileUploadContext = createContext<FileUploadContextValue | null>(null);\n\nfunction useFileUpload() {\n  const context = useContext(FileUploadContext);\n  if (!context) {\n    throw new Error('useFileUpload must be used within a FileUpload');\n  }\n  return context;\n}\n\nfunction hasDraggedFiles(event: DragEvent) {\n  const types = event.dataTransfer?.types;\n  if (!types) {\n    return false;\n  }\n  return Array.from(types).includes('Files');\n}\n\nfunction filesOf(list: FileList, multiple: boolean) {\n  const files = Array.from(list);\n  if (multiple) {\n    return files;\n  }\n  return files.slice(0, 1);\n}\n\ntype FileUploadProps = {\n  onFilesAdded: (files: File[]) => void;\n  children: React.ReactNode;\n  multiple?: boolean;\n  accept?: string;\n  disabled?: boolean;\n};\n\n/** Listens for drops on the whole window, not only on its children. */\nfunction FileUpload({\n  onFilesAdded,\n  children,\n  multiple = true,\n  accept,\n  disabled = false,\n}: FileUploadProps) {\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const dragDepth = useRef(0);\n  const [isDragging, setIsDragging] = useState(false);\n\n  const addFiles = useEffectEvent((list: FileList) => {\n    const files = filesOf(list, multiple);\n    onFilesAdded(files);\n  });\n\n  useEffect(() => {\n    if (disabled) {\n      return;\n    }\n\n    function holdDrag(event: DragEvent) {\n      if (!hasDraggedFiles(event)) {\n        return;\n      }\n      event.preventDefault();\n    }\n\n    function enterDrag(event: DragEvent) {\n      if (!hasDraggedFiles(event)) {\n        return;\n      }\n      event.preventDefault();\n      dragDepth.current += 1;\n      setIsDragging(true);\n    }\n\n    function leaveDrag(event: DragEvent) {\n      if (!hasDraggedFiles(event)) {\n        return;\n      }\n      dragDepth.current = Math.max(0, dragDepth.current - 1);\n      if (dragDepth.current === 0) {\n        setIsDragging(false);\n      }\n    }\n\n    function dropFiles(event: DragEvent) {\n      if (!hasDraggedFiles(event)) {\n        return;\n      }\n      event.preventDefault();\n      dragDepth.current = 0;\n      setIsDragging(false);\n      const list = event.dataTransfer?.files;\n      if (list?.length) {\n        addFiles(list);\n      }\n    }\n\n    window.addEventListener('dragenter', enterDrag);\n    window.addEventListener('dragleave', leaveDrag);\n    window.addEventListener('dragover', holdDrag);\n    window.addEventListener('drop', dropFiles);\n\n    return () => {\n      window.removeEventListener('dragenter', enterDrag);\n      window.removeEventListener('dragleave', leaveDrag);\n      window.removeEventListener('dragover', holdDrag);\n      window.removeEventListener('drop', dropFiles);\n      dragDepth.current = 0;\n    };\n  }, [disabled]);\n\n  function pickFiles(event: React.ChangeEvent<HTMLInputElement>) {\n    const list = event.target.files;\n    if (!list?.length) {\n      return;\n    }\n    const files = filesOf(list, multiple);\n    onFilesAdded(files);\n    event.target.value = '';\n  }\n\n  function openPicker() {\n    inputRef.current?.click();\n  }\n\n  const context: FileUploadContextValue = {\n    isDragging,\n    openPicker,\n    multiple,\n    disabled,\n  };\n\n  return (\n    <FileUploadContext.Provider value={context}>\n      <input\n        ref={inputRef}\n        type='file'\n        tabIndex={-1}\n        aria-hidden='true'\n        className='hidden'\n        multiple={multiple}\n        accept={accept}\n        disabled={disabled}\n        onChange={pickFiles}\n      />\n      {children}\n    </FileUploadContext.Provider>\n  );\n}\n\ntype FileUploadTriggerProps = useRender.ComponentProps<'button'>;\n\n/** Opens the file picker. Render it as one of your buttons with `render={<Button />}`. */\nfunction FileUploadTrigger({ render, ...props }: FileUploadTriggerProps) {\n  const { openPicker, disabled } = useFileUpload();\n\n  function openFromClick(event: React.MouseEvent<HTMLButtonElement>) {\n    if (event.defaultPrevented) {\n      return;\n    }\n    openPicker();\n  }\n\n  const defaultProps = {\n    'data-slot': 'file-upload-trigger',\n    type: 'button' as const,\n    disabled,\n    onClick: openFromClick,\n  };\n\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps<'button'>(defaultProps, props),\n  });\n}\n\ntype PresenceProps = Pick<\n  MotionProps,\n  'initial' | 'animate' | 'exit' | 'transition'\n>;\n\nconst ENTER: Transition = { duration: 0.15, ease: [0.23, 1, 0.32, 1] };\n\nconst EXIT: Transition = { duration: 0.1, ease: [0.23, 1, 0.32, 1] };\n\nconst SETTLE_PRESENCE: PresenceProps = {\n  initial: { opacity: 0, scale: 0.98 },\n  animate: { opacity: 1, scale: 1 },\n  exit: { opacity: 0, scale: 0.98, transition: EXIT },\n  transition: ENTER,\n};\n\nconst FADE_PRESENCE: PresenceProps = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0, transition: EXIT },\n  transition: ENTER,\n};\n\nfunction presenceOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_PRESENCE;\n  }\n  return SETTLE_PRESENCE;\n}\n\nfunction subscribeToNothing() {\n  return () => undefined;\n}\n\nfunction isOnClient() {\n  return true;\n}\n\nfunction isOnServer() {\n  return false;\n}\n\ntype FileUploadContentProps = React.ComponentProps<'div'>;\n\nfunction FileUploadContent({ className, children }: FileUploadContentProps) {\n  const { isDragging, disabled } = useFileUpload();\n  const shouldReduceMotion = useReducedMotion();\n  const presence = presenceOf(shouldReduceMotion);\n  const isMounted = useSyncExternalStore(\n    subscribeToNothing,\n    isOnClient,\n    isOnServer\n  );\n  const isShown = isDragging && !disabled;\n\n  if (!isMounted) {\n    return null;\n  }\n\n  const layer = (\n    <AnimatePresence>\n      {isShown && (\n        <motion.div\n          key='file-upload-content'\n          data-slot='file-upload-content'\n          className={cn(\n            'bg-background/85 pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-6',\n            className\n          )}\n          {...presence}\n        >\n          {children}\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n\n  return createPortal(layer, document.body);\n}\n\nexport {\n  FileUpload,\n  FileUploadContent,\n  FileUploadTrigger,\n  useFileUpload,\n  type FileUploadProps,\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "chat"
  ],
  "type": "registry:ui"
}