@@ -52,6 +52,7 @@ import { ChipTextarea } from '@/components/emcn/components/chip-textarea/chip-te
5252import { Label } from '@/components/emcn/components/label/label'
5353import { Modal , ModalContent } from '@/components/emcn/components/modal/modal'
5454import { TagInput , type TagItem } from '@/components/emcn/components/tag-input/tag-input'
55+ import { Loader } from '@/components/emcn/icons'
5556import { cn } from '@/lib/core/utils/cn'
5657import { quickValidateEmail } from '@/lib/messaging/email/validation'
5758
@@ -377,6 +378,14 @@ interface ChipModalFileFieldProps extends ChipModalFieldBaseProps {
377378 * for a single-line zone.
378379 */
379380 description ?: React . ReactNode
381+ /**
382+ * Renders a spinner inside the drop zone and blocks further picks while an
383+ * async import/upload is in flight. Use for slow selections (zip extraction,
384+ * remote fetches) where the zone would otherwise look idle. Pair with a
385+ * `label` such as `'Importing…'` for an explicit status line.
386+ * @default false
387+ */
388+ loading ?: boolean
380389}
381390
382391export interface ChipModalEmailsFieldProps extends ChipModalFieldBaseProps {
@@ -692,6 +701,7 @@ function ChipModalFileControl({
692701 multiple = false ,
693702 label = 'Drop files here or click to browse' ,
694703 description,
704+ loading = false ,
695705 disabled,
696706 id,
697707 'aria-required' : ariaRequired ,
@@ -700,6 +710,7 @@ function ChipModalFileControl({
700710} : ChipModalFileFieldProps & { id : string } & React . AriaAttributes ) {
701711 const inputRef = React . useRef < HTMLInputElement > ( null )
702712 const [ isDragging , setIsDragging ] = React . useState ( false )
713+ const isInteractive = ! disabled && ! loading
703714
704715 const emitFiles = React . useCallback (
705716 ( files : FileList | null ) => {
@@ -713,15 +724,16 @@ function ChipModalFileControl({
713724 < button
714725 type = 'button'
715726 id = { id }
716- disabled = { disabled }
727+ disabled = { ! isInteractive }
728+ aria-busy = { loading || undefined }
717729 aria-required = { ariaRequired }
718730 aria-invalid = { ariaInvalid }
719731 aria-describedby = { ariaDescribedby }
720732 onClick = { ( ) => inputRef . current ?. click ( ) }
721733 onDragEnter = { ( event ) => {
722734 event . preventDefault ( )
723735 event . stopPropagation ( )
724- if ( ! disabled ) setIsDragging ( true )
736+ if ( isInteractive ) setIsDragging ( true )
725737 } }
726738 onDragOver = { ( event ) => {
727739 event . preventDefault ( )
@@ -736,7 +748,7 @@ function ChipModalFileControl({
736748 event . preventDefault ( )
737749 event . stopPropagation ( )
738750 setIsDragging ( false )
739- if ( ! disabled ) emitFiles ( event . dataTransfer . files )
751+ if ( isInteractive ) emitFiles ( event . dataTransfer . files )
740752 } }
741753 className = { cn (
742754 'flex w-full flex-col items-center justify-center gap-0.5 rounded-lg border border-[var(--border-1)] border-dashed bg-[var(--surface-5)] px-2 py-2.5 text-center outline-none transition-colors hover-hover:border-[var(--surface-7)] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[var(--surface-4)]' ,
@@ -748,13 +760,14 @@ function ChipModalFileControl({
748760 type = 'file'
749761 accept = { accept }
750762 multiple = { multiple }
751- disabled = { disabled }
763+ disabled = { ! isInteractive }
752764 className = 'hidden'
753765 onChange = { ( event ) => {
754766 emitFiles ( event . target . files )
755767 event . target . value = ''
756768 } }
757769 />
770+ { loading ? < Loader animate className = 'size-[14px] text-[var(--text-tertiary)]' /> : null }
758771 < span className = 'text-[var(--text-primary)] text-caption' >
759772 { isDragging ? 'Drop files here' : label }
760773 </ span >
0 commit comments