{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-attachments",
  "files": [
    {
      "path": "src/registry/hooks/use-attachments.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type AttachmentItem = {\n  id: string;\n  file: File;\n  name: string;\n  size: number;\n  type: string;\n  /** Object URL for image files. Revoked automatically on remove or unmount. */\n  previewUrl?: string;\n  status: \"uploading\" | \"done\" | \"error\";\n  /** 0–100 while uploading. */\n  progress: number;\n  error?: string;\n};\n\nexport type UseAttachmentsOptions = {\n  /** Same syntax as the input accept attribute, e.g. \"image/*,.pdf\". */\n  accept?: string;\n  /** Max size per file in bytes. */\n  maxSize?: number;\n  maxFiles?: number;\n  /** Upload hook. Report progress 0–100, resolve when done, reject to mark failed. */\n  upload?: (file: File, onProgress: (percent: number) => void, signal: AbortSignal) => Promise<void>;\n};\n\nfunction matchesAccept(file: File, accept?: string) {\n  if (!accept) return true;\n  return accept.split(\",\").some((raw) => {\n    const rule = raw.trim().toLowerCase();\n    if (!rule) return false;\n    if (rule.startsWith(\".\")) return file.name.toLowerCase().endsWith(rule);\n    if (rule.endsWith(\"/*\")) return file.type.toLowerCase().startsWith(rule.slice(0, -1));\n    return file.type.toLowerCase() === rule;\n  });\n}\n\nfunction formatLimit(bytes: number) {\n  return bytes >= 1024 * 1024 ? `${Math.round(bytes / 1024 / 1024)} MB` : `${Math.round(bytes / 1024)} KB`;\n}\n\nexport function useAttachments({ accept, maxSize, maxFiles, upload }: UseAttachmentsOptions = {}) {\n  const [items, setItems] = React.useState<AttachmentItem[]>([]);\n  const itemsRef = React.useRef(items);\n  React.useEffect(() => {\n    itemsRef.current = items;\n  }, [items]);\n  const controllers = React.useRef(new Map<string, AbortController>());\n\n  const patch = React.useCallback((id: string, next: Partial<AttachmentItem>) => {\n    setItems((prev) => prev.map((it) => (it.id === id ? { ...it, ...next } : it)));\n  }, []);\n\n  const add = React.useCallback(\n    (files: File[]) => {\n      let slots = maxFiles === undefined ? Infinity : Math.max(0, maxFiles - itemsRef.current.length);\n      const created: AttachmentItem[] = [];\n\n      for (const file of files) {\n        const base = {\n          id: crypto.randomUUID(),\n          file,\n          name: file.name,\n          size: file.size,\n          type: file.type,\n          progress: 0,\n        };\n        let error: string | undefined;\n        if (slots <= 0) error = `Limit of ${maxFiles} files reached`;\n        else if (!matchesAccept(file, accept)) error = \"File type not allowed\";\n        else if (maxSize !== undefined && file.size > maxSize) error = `Larger than ${formatLimit(maxSize)}`;\n\n        if (error) {\n          created.push({ ...base, status: \"error\", error });\n          continue;\n        }\n        slots -= 1;\n        created.push({\n          ...base,\n          previewUrl: file.type.startsWith(\"image/\") ? URL.createObjectURL(file) : undefined,\n          status: upload ? \"uploading\" : \"done\",\n          progress: upload ? 0 : 100,\n        });\n      }\n\n      setItems((prev) => [...prev, ...created]);\n\n      if (!upload) return;\n      for (const item of created) {\n        if (item.status !== \"uploading\") continue;\n        const controller = new AbortController();\n        controllers.current.set(item.id, controller);\n        upload(item.file, (p) => patch(item.id, { progress: Math.round(p) }), controller.signal)\n          .then(() => patch(item.id, { status: \"done\", progress: 100 }))\n          .catch((e: unknown) => {\n            if (controller.signal.aborted) return;\n            patch(item.id, { status: \"error\", error: e instanceof Error ? e.message : \"Upload failed\" });\n          })\n          .finally(() => controllers.current.delete(item.id));\n      }\n    },\n    [accept, maxSize, maxFiles, upload, patch],\n  );\n\n  const remove = React.useCallback((id: string) => {\n    controllers.current.get(id)?.abort();\n    controllers.current.delete(id);\n    setItems((prev) => {\n      const target = prev.find((it) => it.id === id);\n      if (target?.previewUrl) URL.revokeObjectURL(target.previewUrl);\n      return prev.filter((it) => it.id !== id);\n    });\n  }, []);\n\n  const clear = React.useCallback(() => {\n    for (const c of controllers.current.values()) c.abort();\n    controllers.current.clear();\n    for (const it of itemsRef.current) if (it.previewUrl) URL.revokeObjectURL(it.previewUrl);\n    setItems([]);\n  }, []);\n\n  React.useEffect(\n    () => () => {\n      for (const c of controllers.current.values()) c.abort();\n      for (const it of itemsRef.current) if (it.previewUrl) URL.revokeObjectURL(it.previewUrl);\n    },\n    [],\n  );\n\n  return {\n    items,\n    add,\n    remove,\n    clear,\n    isUploading: items.some((it) => it.status === \"uploading\"),\n    /** Files that finished successfully, ready to send. */\n    ready: items.filter((it) => it.status === \"done\"),\n  };\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}