{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "title": "File Upload",
  "description": "File Upload from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/file-icon.json",
    "https://zero-design.itdone.de/r/labels.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/loading-spinner.json",
    "https://zero-design.itdone.de/r/progress.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/file-upload.tsx",
      "content": "'use client';\n\nimport { CheckCircle2, RotateCcw, Trash2, UploadCloud } from 'lucide-react';\nimport * as React from 'react';\n\nimport { focusRing } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { FileIcon } from '@/components/ui/file-icon';\nimport { LoadingSpinner } from '@/components/ui/loading-spinner';\nimport { Progress } from '@/components/ui/progress';\nimport { useLabels, useLocale, usePreset, type DesignSystemLocale } from '@/components/ui/labels-context';\nimport type { LabelProp } from '@/components/ui/labels';\n\nexport interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'title'> {\n  accept?: string;\n  multiple?: boolean;\n  disabled?: boolean;\n  name?: string;\n  // Called with the chosen or dropped files. Without `multiple` only the first file is passed.\n  onFilesSelected?: (files: File[]) => void;\n  // Emphasised lead-in of the drop zone, for example \"Click to upload\". `titleText` is the string form of the same text.\n  title?: React.ReactNode;\n  titleText?: string;\n  description?: React.ReactNode;\n  descriptionText?: string;\n  // Supporting text such as the accepted types and size limit.\n  hint?: React.ReactNode;\n  // Upload in progress: swaps the title for `loadingTitle` and announces it politely.\n  loading?: boolean;\n  loadingTitle?: React.ReactNode;\n  loadingText?: string;\n  // Validation message. Sets `aria-invalid` on the input and replaces the hint.\n  error?: React.ReactNode;\n  // `zone` (default) is the always visible drop zone. `overlay` shows nothing until a file is dragged over the page, then covers the nearest positioned ancestor with the drop target. Offer a button that calls `ref.current.click()` for keyboard users: the input is not tabbable in this mode.\n  mode?: 'zone' | 'overlay';\n  // External trigger: receives `open()`, which opens the file dialog, and renders before the zone. Use it for an \"Upload\" button elsewhere in a toolbar, and pair it with `mode=\"overlay\"` for a drop-anywhere area. `open` does nothing while the field is disabled or loading.\n  trigger?: (open: () => void) => React.ReactNode;\n  // Queued files: render `FileUploadItem` elements.\n  children?: React.ReactNode;\n}\n\nconst FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(\n  (\n    {\n      className,\n      accept,\n      multiple = false,\n      disabled = false,\n      name,\n      onFilesSelected,\n      title: titleProp,\n      titleText,\n      description: descriptionProp,\n      descriptionText,\n      hint,\n      loading = false,\n      loadingTitle: loadingTitleProp,\n      loadingText,\n      error,\n      mode = 'zone',\n      trigger,\n      children,\n      id,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels();\n    const zero = usePreset() === 'zero';\n    const title = titleProp ?? titleText ?? l.fileUploadTitle;\n    const description = descriptionProp ?? descriptionText ?? l.fileUploadDescription;\n    const loadingTitle = loadingTitleProp ?? loadingText ?? l.fileUploadLoading;\n    const innerRef = React.useRef<HTMLInputElement | null>(null);\n    const setRefs = React.useCallback(\n      (node: HTMLInputElement | null) => {\n        innerRef.current = node;\n        if (typeof ref === 'function') ref(node);\n        else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;\n      },\n      [ref],\n    );\n    const open = () => {\n      if (disabled || loading) return;\n      innerRef.current?.click();\n    };\n    const autoId = React.useId();\n    const inputId = id ?? `${autoId}-file`;\n    const messageId = `${inputId}-message`;\n    const titleId = `${inputId}-title`;\n    const [dragging, setDragging] = React.useState(false);\n    const overlay = mode === 'overlay';\n    // The drop target fills with the brand tint while a file is over it (always in overlay mode), so its text switches to the on-tint roles.\n    const tinted = (dragging || overlay) && !disabled;\n    const [pageDragging, setPageDragging] = React.useState(false);\n    // Overlay mode: watch the whole page for files being dragged, counting enter and leave pairs of nested elements.\n    React.useEffect(() => {\n      if (!overlay || disabled) return undefined;\n      let depth = 0;\n      const hasFiles = (event: DragEvent) => Array.from(event.dataTransfer?.types ?? []).includes('Files');\n      const enter = (event: DragEvent) => {\n        if (!hasFiles(event)) return;\n        depth += 1;\n        setPageDragging(true);\n      };\n      const leave = (event: DragEvent) => {\n        if (!hasFiles(event)) return;\n        depth = Math.max(0, depth - 1);\n        if (depth === 0) setPageDragging(false);\n      };\n      const reset = () => {\n        depth = 0;\n        setPageDragging(false);\n      };\n      document.addEventListener('dragenter', enter);\n      document.addEventListener('dragleave', leave);\n      document.addEventListener('drop', reset);\n      document.addEventListener('dragend', reset);\n      return () => {\n        document.removeEventListener('dragenter', enter);\n        document.removeEventListener('dragleave', leave);\n        document.removeEventListener('drop', reset);\n        document.removeEventListener('dragend', reset);\n      };\n    }, [overlay, disabled]);\n    const hasError = error != null && error !== false && error !== '';\n    const message = hasError ? error : hint;\n\n    const emit = (list: FileList | null) => {\n      if (!list || list.length === 0) return;\n      const files = Array.from(list);\n      onFilesSelected?.(multiple ? files : files.slice(0, 1));\n    };\n\n    return (\n      <div className={cn(overlay ? 'contents' : 'flex w-full flex-col gap-3', className)} data-mode={mode} aria-busy={loading || undefined} {...props}>\n        {trigger ? trigger(open) : null}\n        <input\n          ref={setRefs}\n          id={inputId}\n          type=\"file\"\n          name={name}\n          accept={accept}\n          multiple={multiple}\n          disabled={disabled}\n          aria-invalid={hasError || undefined}\n          aria-labelledby={titleId}\n          aria-describedby={message ? messageId : undefined}\n          tabIndex={overlay ? -1 : undefined}\n          className=\"peer sr-only\"\n          onChange={(event) => {\n            emit(event.target.files);\n            event.target.value = '';\n          }}\n        />\n        <label\n          htmlFor={inputId}\n          data-dragging={dragging || undefined}\n          data-invalid={hasError || undefined}\n          data-disabled={disabled || undefined}\n          data-loading={loading || undefined}\n          onDragEnter={(event) => {\n            if (disabled) return;\n            event.preventDefault();\n            setDragging(true);\n          }}\n          onDragOver={(event) => {\n            if (disabled) return;\n            event.preventDefault();\n            setDragging(true);\n          }}\n          onDragLeave={() => setDragging(false)}\n          onDrop={(event) => {\n            event.preventDefault();\n            setDragging(false);\n            setPageDragging(false);\n            if (disabled) return;\n            emit(event.dataTransfer?.files ?? null);\n          }}\n          className={cn(\n            'flex w-full cursor-pointer flex-col items-center gap-3 rounded-surface border border-solid text-center',\n            // Zero: dashed drop zone on the muted fill (32/24 padding), the other themes keep the plain surface.\n            zero ? 'border-dashed border-border-bold bg-muted px-6 py-8' : 'border-border-default bg-background px-6 py-4',\n            'transition-[color,background-color,border-color,box-shadow] duration-150 motion-reduce:transition-none',\n            'hover:border-border-bold',\n            'peer-focus-visible:border-border-focus peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/30',\n            'data-[dragging=true]:border-border-brand data-[dragging=true]:bg-background-brand-subtle',\n            'data-[invalid=true]:border-border-destructive',\n            'data-[disabled=true]:pointer-events-none data-[disabled=true]:cursor-not-allowed data-[disabled=true]:border-border-disabled data-[disabled=true]:bg-muted data-[disabled=true]:text-text-disabled',\n            // Overlay: covers the positioned ancestor, only while a file is dragged over the page.\n            overlay && 'absolute inset-0 z-raised justify-center border-dashed border-border-brand bg-background-brand-subtle',\n            overlay && !pageDragging && 'hidden',\n          )}\n        >\n          <span\n            aria-hidden=\"true\"\n            className=\"inline-flex h-10 w-10 items-center justify-center rounded-full bg-muted text-icon-subtle\"\n          >\n            <UploadCloud className=\"h-icon-sm w-icon-sm\" />\n          </span>\n          <span className=\"flex flex-col items-center gap-1\">\n            <span className={cn('typography-ui', tinted ? 'text-general-brand-on-container' : zero ? 'text-text-subtle-on-muted' : 'text-text-subtle')}>\n              <span id={titleId} className={cn('typography-ui-semibold', disabled ? 'text-text-disabled' : tinted || zero ? 'text-text-brand-on-subtle' : 'text-text-brand')}>\n                {loading ? loadingTitle : title}\n              </span>\n              {loading ? null : <> {description}</>}\n            </span>\n            {message ? (\n              <span\n                id={messageId}\n                className={cn('typography-caption', hasError ? (tinted || zero ? 'text-text-destructive-on-subtle' : 'text-text-destructive') : tinted ? 'text-general-brand-on-container' : zero ? 'text-text-subtle-on-muted' : 'text-text-subtle')}\n              >\n                {message}\n              </span>\n            ) : null}\n          </span>\n        </label>\n        <span role=\"status\" className=\"sr-only\">\n          {loading && typeof loadingTitle === 'string' ? loadingTitle : ''}\n        </span>\n        {children ? <ul className=\"flex flex-col gap-2\">{children}</ul> : null}\n      </div>\n    );\n  },\n);\nFileUpload.displayName = 'FileUpload';\n\nexport type FileUploadItemKind = 'document' | 'image' | 'video' | 'other';\nexport type FileUploadItemStatus = 'selected' | 'uploading' | 'complete' | 'error';\n\n\nconst formatBytes = (bytes: number, locale: DesignSystemLocale) => {\n  if (bytes < 1024) return `${bytes} B`;\n  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;\n  const mb = (bytes / (1024 * 1024)).toFixed(1).replace(/\\.0$/, '');\n  return `${locale === 'de' ? mb.replace('.', ',') : mb} MB`;\n};\n\nexport interface FileUploadItemProps extends Omit<React.HTMLAttributes<HTMLLIElement>, 'children'> {\n  name: string;\n  // Bytes (formatted for you) or a ready string such as \"4.2 MB\".\n  size?: number | string;\n  // Overrides the file type shown in the tile. Without it the type comes from the file name extension.\n  kind?: FileUploadItemKind;\n  status?: FileUploadItemStatus;\n  // Upload progress 0..100 while `status` is uploading.\n  progress?: number;\n  // `bar` shows a progress bar under the name, `fill` tints the row from the left and shows a spinner.\n  variant?: 'bar' | 'fill';\n  // Message shown instead of the size while `status` is error.\n  error?: React.ReactNode;\n  onRemove?: () => void;\n  onRetry?: () => void;\n  // Accessible names, a string or a function of the file name. Win over the provider text.\n  removeLabel?: LabelProp<[string | undefined]>;\n  retryLabel?: LabelProp<[string]>;\n  uploadingLabel?: LabelProp<[string]>;\n  completeLabel?: string;\n  // Visible texts. Win over the provider text.\n  errorText?: string;\n  uploadedText?: LabelProp<[number]>;\n  // Visible text for the `selected` status (chosen, not uploaded yet). Wins over the provider text.\n  notUploadedText?: string;\n}\n\nconst FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(\n  (\n    {\n      className,\n      name,\n      size,\n      kind,\n      status = 'uploading',\n      progress = 0,\n      variant = 'bar',\n      error,\n      onRemove,\n      onRetry,\n      removeLabel,\n      retryLabel,\n      uploadingLabel,\n      completeLabel,\n      errorText,\n      uploadedText,\n      notUploadedText,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels({\n      remove: removeLabel,\n      retryItem: retryLabel,\n      uploading: uploadingLabel,\n      uploadComplete: completeLabel,\n      uploadFailed: errorText,\n      uploadedPercent: uploadedText,\n      fileNotUploaded: notUploadedText,\n    });\n    const locale = useLocale();\n    const zero = usePreset() === 'zero';\n    const percent = Math.round(Math.min(Math.max(progress, 0), 100));\n    const sizeText = typeof size === 'number' ? formatBytes(size, locale) : size;\n    const uploading = status === 'uploading';\n    const meta =\n      status === 'error'\n        ? (error ?? l.uploadFailed)\n        : status === 'selected'\n          ? [sizeText, l.fileNotUploaded].filter(Boolean).join(' - ')\n            : [sizeText, variant === 'fill' && uploading ? l.uploadedPercent(percent) : null].filter(Boolean).join(' - ');\n    const actionClass = cn(\n      'inline-flex h-control-sm w-control-sm shrink-0 items-center justify-center rounded-item text-icon-subtle transition-colors hover:bg-muted hover:text-icon-default',\n      focusRing,\n    );\n\n    return (\n      <li\n        ref={ref}\n        data-status={status}\n        data-variant={variant}\n        className={cn(\n          'relative flex items-start gap-3 overflow-hidden rounded-surface border border-solid bg-background',\n          zero ? 'px-4 py-3' : 'p-4',\n          status === 'error' ? 'border-border-destructive' : 'border-border-default',\n          className,\n        )}\n        {...props}\n      >\n        {variant === 'fill' && uploading ? (\n          <span\n            role=\"progressbar\"\n            aria-label={l.uploading(name)}\n            aria-valuemin={0}\n            aria-valuemax={100}\n            aria-valuenow={percent}\n            data-progress-fill=\"\"\n            style={{ width: `${percent}%` }}\n            className=\"absolute inset-y-0 left-0 bg-muted transition-[width] duration-150 motion-reduce:transition-none\"\n          />\n        ) : null}\n        <FileIcon name={name} kind={kind} error={status === 'error'} size=\"xs\" className=\"relative\" />\n        <div className=\"relative flex min-w-0 flex-1 flex-col gap-1\">\n          <span className=\"truncate typography-ui-medium text-foreground\" title={name}>\n            {name}\n          </span>\n          <span className={cn('typography-caption', status === 'error' ? 'text-text-destructive' : variant === 'fill' && uploading ? 'text-text-subtle-on-muted' : 'text-text-subtle')}>{meta}</span>\n          {variant === 'bar' && status !== 'error' && status !== 'selected' ? (\n            <div className=\"flex items-center gap-3\">\n              <Progress\n                aria-label={l.uploading(name)}\n                value={status === 'complete' ? 100 : percent}\n                className=\"flex-1\"\n              />\n              <span className=\"typography-caption w-8 shrink-0 text-right tabular-nums text-text-subtle\">\n                {status === 'complete' ? 100 : percent}%\n              </span>\n            </div>\n          ) : null}\n          {status === 'complete' ? <span className=\"sr-only\">{l.uploadComplete}</span> : null}\n        </div>\n        <div className=\"relative flex shrink-0 items-center gap-1\">\n          {variant === 'fill' && uploading ? <LoadingSpinner size=\"sm\" label={l.uploading(name)} /> : null}\n          {status === 'complete' ? <CheckCircle2 aria-hidden=\"true\" className=\"h-icon-sm w-icon-sm text-icon-brand\" /> : null}\n          {status === 'error' && onRetry ? (\n            <button type=\"button\" className={actionClass} aria-label={l.retryItem(name)} onClick={onRetry}>\n              <RotateCcw aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs\" />\n            </button>\n          ) : null}\n          {onRemove ? (\n            <button type=\"button\" className={actionClass} aria-label={l.remove(name)} onClick={onRemove}>\n              <Trash2 aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs\" />\n            </button>\n          ) : null}\n        </div>\n      </li>\n    );\n  },\n);\nFileUploadItem.displayName = 'FileUploadItem';\n\nexport { FileUpload, FileUploadItem };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}