{"version":3,"file":"FileUpload.cjs","names":[],"sources":["../../../src/components/FileUpload/FileUpload.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the picker (accept,\n * multiple, maxSize, onReject) and the list of chosen files (value, onChange,\n * disabled) are one control, and title/subtitle/label exist because the empty state\n * is the biggest part of the surface.\n */\nimport { useRef, useState } from \"react\";\nimport type { DragEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./FileUpload.module.css\";\n\nexport interface FileUploadProps {\n    /** Files currently in the dropzone (controlled). */\n    value: File[];\n    /** Called with the new list when the user adds or removes a file. */\n    onChange: (files: File[]) => void;\n    label?: string;\n    /** Accept attribute forwarded to `<input>`. */\n    accept?: string;\n    /** Allow multiple files. Default: false. */\n    multiple?: boolean;\n    /** Max file size in bytes. Files above are rejected via `onReject`. */\n    maxSize?: number;\n    /** Called with rejected files when they exceed `maxSize` or fail the `accept` filter. */\n    onReject?: (rejected: { file: File; reason: \"size\" | \"type\" }[]) => void;\n    disabled?: boolean;\n    /** Title shown on the empty dropzone. */\n    title?: string;\n    /** Helper line below the title. */\n    subtitle?: string;\n    className?: string;\n}\n\nfunction formatSize(bytes: number): string {\n    if (bytes < 1024) return `${bytes} B`;\n    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n    return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n}\n\nfunction matchesAccept(file: File, accept?: string): boolean {\n    if (!accept) return true;\n    const tokens = accept.split(\",\").map((t) => t.trim().toLowerCase());\n    const name = file.name.toLowerCase();\n    const type = file.type.toLowerCase();\n    return tokens.some((token) => {\n        if (token.startsWith(\".\")) return name.endsWith(token);\n        if (token.endsWith(\"/*\")) return type.startsWith(token.replace(\"/*\", \"/\"));\n        return type === token;\n    });\n}\n\n/**\n * Drag-and-drop file dropzone. Pair with `uploadWithProgress` from the SDK\n * to wire actual uploads with byte-level progress.\n */\nexport function FileUpload({\n    value,\n    onChange,\n    label,\n    accept,\n    multiple = false,\n    maxSize,\n    onReject,\n    disabled,\n    title = \"Arraste arquivos aqui ou clique para selecionar\",\n    subtitle,\n    className,\n}: FileUploadProps) {\n    const [dragging, setDragging] = useState<boolean>(false);\n    const inputRef = useRef<HTMLInputElement>(null);\n\n    function addFiles(incoming: FileList | File[]): void {\n        const accepted: File[] = [];\n        const rejected: { file: File; reason: \"size\" | \"type\" }[] = [];\n\n        for (const file of Array.from(incoming)) {\n            if (!matchesAccept(file, accept)) {\n                rejected.push({ file, reason: \"type\" });\n                continue;\n            }\n            if (maxSize && file.size > maxSize) {\n                rejected.push({ file, reason: \"size\" });\n                continue;\n            }\n            accepted.push(file);\n        }\n\n        if (rejected.length > 0) onReject?.(rejected);\n        if (accepted.length === 0) return;\n\n        const next = multiple ? [...value, ...accepted] : accepted.slice(0, 1);\n        onChange(next);\n    }\n\n    function handleDrop(event: DragEvent<HTMLDivElement>): void {\n        event.preventDefault();\n        setDragging(false);\n        if (disabled) return;\n        if (event.dataTransfer?.files) addFiles(event.dataTransfer.files);\n    }\n\n    function remove(index: number): void {\n        const next = value.slice();\n        next.splice(index, 1);\n        onChange(next);\n    }\n\n    return (\n        <div className={cn(styles.wrapper, className)}>\n            {label && <label className={styles.label}>{label}</label>}\n            <div\n                className={cn(\n                    styles.dropzone,\n                    dragging && styles.active,\n                    disabled && styles.disabled,\n                )}\n                role=\"button\"\n                tabIndex={disabled ? -1 : 0}\n                onClick={() => !disabled && inputRef.current?.click()}\n                onKeyDown={(event) => {\n                    if (event.key === \"Enter\" || event.key === \" \") {\n                        event.preventDefault();\n                        inputRef.current?.click();\n                    }\n                }}\n                onDragEnter={(event) => {\n                    event.preventDefault();\n                    if (!disabled) setDragging(true);\n                }}\n                onDragOver={(event) => event.preventDefault()}\n                onDragLeave={() => setDragging(false)}\n                onDrop={handleDrop}\n            >\n                <div className={styles.icon} aria-hidden>\n                    <UploadIcon />\n                </div>\n                <p className={styles.title}>{title}</p>\n                {subtitle && <p className={styles.subtitle}>{subtitle}</p>}\n                <input\n                    ref={inputRef}\n                    type=\"file\"\n                    accept={accept}\n                    multiple={multiple}\n                    disabled={disabled}\n                    className={styles.hidden}\n                    onChange={(event) => {\n                        if (event.target.files) addFiles(event.target.files);\n                        event.target.value = \"\";\n                    }}\n                />\n            </div>\n\n            {value.length > 0 && (\n                <ul className={styles.files}>\n                    {value.map((file, index) => (\n                        <li key={`${file.name}-${index}`} className={styles.file}>\n                            <div>\n                                <div>{file.name}</div>\n                                <div className={styles.fileMeta}>{formatSize(file.size)}</div>\n                            </div>\n                            <button\n                                type=\"button\"\n                                aria-label={`Remover ${file.name}`}\n                                className={styles.remove}\n                                onClick={() => remove(index)}\n                            >\n                                ×\n                            </button>\n                        </li>\n                    ))}\n                </ul>\n            )}\n        </div>\n    );\n}\n\nfunction UploadIcon() {\n    return (\n        <svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\">\n            <path\n                d=\"M12 16V4m0 0l-4 4m4-4l4 4M4 20h16\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n            />\n        </svg>\n    );\n}\n"],"mappings":"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"}