/*! ratio-ui · design system for knowledge sharing · https://github.com/losol/ratio-ui · (c) 2026 Losol AS · MPL-2.0 */
import { cn } from "../../utils/cn.js";
import { AlertCircle, Check, CloudUpload, File, LoaderCircle, X } from "../../icons/index.js";
import { useEffect, useMemo, useRef } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { Button, DropZone, FileTrigger, Text } from "react-aria-components";
//#region src/forms/FileUpload/FileUpload.tsx
var styles = {
	wrapper: "flex flex-col gap-2 w-full",
	label: "text-sm font-medium text-(--text) cursor-default",
	description: "text-xs text-(--text-muted)",
	dropzone: {
		base: [
			"flex flex-col items-center justify-center gap-2",
			"w-full px-4 py-8 rounded-lg text-center",
			"border-2 border-dashed border-border-1",
			"bg-card text-(--text-subtle)",
			"transition-colors",
			"data-[hovered]:border-(--primary)",
			"data-[focus-visible]:outline-none data-[focus-visible]:ring-2 data-[focus-visible]:ring-(--focus-ring)"
		].join(" "),
		active: "border-(--primary) bg-card-hover text-(--text)",
		disabled: "opacity-50 cursor-not-allowed",
		icon: "h-8 w-8"
	},
	button: [
		"inline-flex items-center gap-2 px-3 py-1.5 rounded-lg",
		"border border-border-1 bg-card text-(--text) text-sm font-medium",
		"hover:border-(--primary)",
		"data-[focus-visible]:outline-none data-[focus-visible]:ring-2 data-[focus-visible]:ring-(--focus-ring)",
		"data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed",
		"transition-colors"
	].join(" "),
	list: "flex flex-col gap-2",
	item: {
		base: "flex items-center gap-3 p-2 rounded-lg border border-border-1 bg-card",
		thumb: "h-10 w-10 shrink-0 rounded object-cover bg-card-hover",
		icon: "h-10 w-10 shrink-0 rounded bg-card-hover p-2 text-(--text-subtle)",
		body: "min-w-0 flex-1",
		name: "truncate text-sm text-(--text)",
		meta: "text-xs text-(--text-muted)",
		error: "text-xs text-error-text",
		remove: [
			"shrink-0 rounded p-1 text-(--text-subtle)",
			"hover:text-(--text) hover:bg-card-hover",
			"focus:outline-none focus:ring-2 focus:ring-(--focus-ring)",
			"disabled:opacity-50 disabled:cursor-not-allowed"
		].join(" ")
	},
	progress: {
		track: "mt-1 h-1 w-full overflow-hidden rounded-full bg-card-hover",
		bar: "h-full rounded-full bg-(--primary) transition-[width] duration-200"
	}
};
var isImage = (file) => file.type.startsWith("image/");
var formatBytes = (bytes) => {
	if (bytes < 1024) return `${bytes} B`;
	const units = [
		"KB",
		"MB",
		"GB"
	];
	let value = bytes / 1024;
	let unit = 0;
	while (value >= 1024 && unit < units.length - 1) {
		value /= 1024;
		unit += 1;
	}
	return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
};
var matchesAccept = (file, accept) => {
	if (!accept || accept.length === 0) return true;
	const name = file.name.toLowerCase();
	const type = file.type;
	return accept.some((rule) => {
		const r = rule.trim().toLowerCase();
		if (r.startsWith(".")) return name.endsWith(r);
		if (r.endsWith("/*")) return type.startsWith(r.slice(0, -1));
		return type === r;
	});
};
/**
* Thumbnail that owns its object URL: created on mount, revoked on unmount.
* Falls back to a generic file icon for non-image files.
*/
var FilePreview = ({ file, thumbClassName, iconClassName }) => {
	const imgRef = useRef(null);
	useEffect(() => {
		const img = imgRef.current;
		if (!img || !isImage(file)) return;
		const objectUrl = URL.createObjectURL(file);
		img.src = objectUrl;
		return () => URL.revokeObjectURL(objectUrl);
	}, [file]);
	if (isImage(file)) return /* @__PURE__ */ jsx("img", {
		ref: imgRef,
		alt: file.name,
		className: thumbClassName
	});
	return /* @__PURE__ */ jsx(File, {
		className: iconClassName,
		"aria-hidden": "true"
	});
};
/**
* Flatten dropped items into a `File[]`, recursing into directories when
* `includeDirectories` is set. Works for both the sync `e.items` array and the
* async iterable returned by `DirectoryDropItem.getEntries()`.
*/
var collectDroppedFiles = async (items, includeDirectories) => {
	const files = [];
	for await (const item of items) if (item.kind === "file") files.push(await item.getFile());
	else if (item.kind === "directory" && includeDirectories) files.push(...await collectDroppedFiles(item.getEntries(), true));
	return files;
};
var StatusIcon = ({ status }) => {
	if (status === "uploading") return /* @__PURE__ */ jsx(LoaderCircle, {
		className: "h-4 w-4 animate-spin text-(--primary)",
		"aria-hidden": "true"
	});
	if (status === "success") return /* @__PURE__ */ jsx(Check, {
		className: "h-4 w-4 text-success-text",
		"aria-hidden": "true"
	});
	if (status === "error") return /* @__PURE__ */ jsx(AlertCircle, {
		className: "h-4 w-4 text-error-text",
		"aria-hidden": "true"
	});
	return null;
};
/**
* FileUpload — accessible file picker with drag-and-drop, image previews and
* upload progress, built on react-aria-components' `DropZone` + `FileTrigger`.
*
* Presentational and transport-agnostic: it emits validated `File[]` via
* `onSelect` and renders the controlled `items` list. The consuming app owns
* the actual upload and feeds back `progress`/`status`/`error` per item.
*
* @beta The API may change in a minor release while it stabilises.
*
* @example
* ```tsx
* <FileUpload
*   items={items}
*   accept={['image/*']}
*   multiple
*   maxSize={5 * 1024 * 1024}
*   onSelect={(files) => uploadAll(files)}
*   onRemove={(id) => cancel(id)}
* />
* ```
*/
var FileUpload = ({ items = [], onSelect, onRemove, onError, accept, multiple = false, acceptDirectory = false, maxSize, isDisabled, label, description, dropzoneLabel = "Drag files here", buttonLabel = "Browse files", className, testId }) => {
	const acceptHint = useMemo(() => accept?.join(", "), [accept]);
	const allowsMany = multiple || acceptDirectory;
	const processFiles = (incoming) => {
		if (incoming.length === 0) return;
		const candidates = allowsMany ? incoming : incoming.slice(0, 1);
		const accepted = [];
		const rejections = [];
		if (!allowsMany) for (const file of incoming.slice(1)) rejections.push({
			file,
			reason: "count"
		});
		for (const file of candidates) if (!matchesAccept(file, accept)) rejections.push({
			file,
			reason: "type"
		});
		else if (maxSize != null && file.size > maxSize) rejections.push({
			file,
			reason: "size"
		});
		else accepted.push(file);
		if (rejections.length > 0) onError?.(rejections);
		if (accepted.length > 0) onSelect(accepted);
	};
	return /* @__PURE__ */ jsxs("div", {
		className: cn(styles.wrapper, className),
		"data-testid": testId,
		children: [
			label && /* @__PURE__ */ jsx("span", {
				className: styles.label,
				children: label
			}),
			/* @__PURE__ */ jsxs(DropZone, {
				isDisabled,
				"aria-label": label ?? "File upload drop zone",
				className: ({ isDropTarget }) => cn(styles.dropzone.base, isDropTarget && styles.dropzone.active, isDisabled && styles.dropzone.disabled),
				onDrop: async (e) => {
					processFiles(await collectDroppedFiles(e.items, acceptDirectory));
				},
				children: [
					/* @__PURE__ */ jsx(CloudUpload, {
						className: styles.dropzone.icon,
						"aria-hidden": "true"
					}),
					/* @__PURE__ */ jsx(Text, {
						slot: "label",
						className: "text-sm",
						children: dropzoneLabel
					}),
					/* @__PURE__ */ jsx(FileTrigger, {
						acceptedFileTypes: accept,
						allowsMultiple: allowsMany,
						acceptDirectory,
						onSelect: (fileList) => {
							if (fileList) processFiles(Array.from(fileList));
						},
						children: /* @__PURE__ */ jsxs(Button, {
							className: styles.button,
							isDisabled,
							children: [/* @__PURE__ */ jsx(CloudUpload, {
								className: "h-4 w-4",
								"aria-hidden": "true"
							}), buttonLabel]
						})
					})
				]
			}),
			(description || acceptHint || maxSize != null) && /* @__PURE__ */ jsx("span", {
				className: styles.description,
				children: description ?? [acceptHint, maxSize != null ? `up to ${formatBytes(maxSize)}` : null].filter(Boolean).join(" · ")
			}),
			items.length > 0 && /* @__PURE__ */ jsx("ul", {
				className: styles.list,
				children: items.map((item) => /* @__PURE__ */ jsxs("li", {
					className: styles.item.base,
					children: [
						/* @__PURE__ */ jsx(FilePreview, {
							file: item.file,
							thumbClassName: styles.item.thumb,
							iconClassName: styles.item.icon
						}),
						/* @__PURE__ */ jsxs("div", {
							className: styles.item.body,
							children: [
								/* @__PURE__ */ jsxs("div", {
									className: "flex items-center gap-2",
									children: [/* @__PURE__ */ jsx("span", {
										className: styles.item.name,
										children: item.file.name
									}), /* @__PURE__ */ jsx(StatusIcon, { status: item.status })]
								}),
								/* @__PURE__ */ jsx("span", {
									className: styles.item.meta,
									children: formatBytes(item.file.size)
								}),
								item.status === "uploading" && /* @__PURE__ */ jsx("div", {
									className: styles.progress.track,
									role: "progressbar",
									"aria-label": `Uploading ${item.file.name}`,
									"aria-valuenow": item.progress ?? 0,
									"aria-valuemin": 0,
									"aria-valuemax": 100,
									children: /* @__PURE__ */ jsx("div", {
										className: styles.progress.bar,
										style: { width: `${item.progress ?? 0}%` }
									})
								}),
								item.status === "error" && item.error && /* @__PURE__ */ jsx("span", {
									className: styles.item.error,
									children: item.error
								})
							]
						}),
						onRemove && /* @__PURE__ */ jsx("button", {
							type: "button",
							className: styles.item.remove,
							onClick: () => onRemove(item.id),
							disabled: isDisabled,
							"aria-label": `Remove ${item.file.name}`,
							children: /* @__PURE__ */ jsx(X, {
								className: "h-4 w-4",
								"aria-hidden": "true"
							})
						})
					]
				}, item.id))
			})
		]
	});
};
FileUpload.displayName = "FileUpload";
//#endregion
export { FileUpload };

//# sourceMappingURL=FileUpload.js.map