{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"lib-files","type":"registry:lib","title":"files helpers","description":"Files picked in a form: their size in words, whether a file is accepted, and a small preview. FileDropzone and","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json"],"files":[{"path":"registry/dashral/lib/files.ts","type":"registry:file","target":"components/dashral/lib/files.ts","content":"\"use client\"\n\n// Files picked in a form: their size in words, whether a file is accepted, and a small preview. FileDropzone and\n// PromptComposer share them.\n\nimport * as React from \"react\"\nimport { FileIcon } from \"lucide-react\"\n\nimport type { useLocale } from \"./locale\"\nimport { cn } from \"./utils\"\n\n/** \"1.4 MB\", in the locale's digits. */\nexport function formatBytes(bytes: number, locale = \"en-US\") {\n  const units = [\"B\", \"KB\", \"MB\", \"GB\"]\n  let value = bytes\n  let unit = 0\n  while (value >= 1024 && unit < units.length - 1) {\n    value /= 1024\n    unit++\n  }\n  return `${value.toLocaleString(locale, { maximumFractionDigits: unit === 0 ? 0 : 1 })} ${units[unit]}`\n}\n\n/** \"image/*,.pdf\" → \"Images, PDF\". */\nexport function acceptLabel(accept: string, t: ReturnType<typeof useLocale>[\"t\"]) {\n  const kinds: Record<string, string> = { image: t(\"images\"), video: t(\"videos\"), audio: t(\"audio\"), text: t(\"textFiles\") }\n  return accept\n    .split(\",\")\n    .map((raw) => {\n      const rule = raw.trim()\n      if (rule.endsWith(\"/*\")) return kinds[rule.slice(0, -2)] ?? rule\n      if (rule.startsWith(\".\")) return rule.slice(1).toUpperCase()\n      return rule.split(\"/\").at(-1)!.toUpperCase()\n    })\n    .join(\", \")\n}\n\n/** Whether a file matches an `accept` list (\"image/*\", \".pdf,.docx\", \"image/png\"); everything does without one. */\nexport function accepts(file: File, accept: string | undefined) {\n  if (!accept) return true\n  return accept.split(\",\").some((raw) => {\n    const rule = raw.trim().toLowerCase()\n    if (rule.startsWith(\".\")) return file.name.toLowerCase().endsWith(rule)\n    if (rule.endsWith(\"/*\")) return file.type.startsWith(rule.slice(0, -1))\n    return file.type === rule\n  })\n}\n\n/** A small preview of an image file, or a file icon. */\nexport function FileThumb({ file, className }: { file: File; className?: string }) {\n  const [url, setUrl] = React.useState<string | null>(null)\n  React.useEffect(() => {\n    if (!file.type.startsWith(\"image/\")) return\n    const next = URL.createObjectURL(file)\n    setUrl(next)\n    return () => URL.revokeObjectURL(next)\n  }, [file])\n  return React.createElement(\n    \"span\",\n    { \"aria-hidden\": true, className: cn(\"flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md border bg-muted text-muted-foreground\", className) },\n    url ? React.createElement(\"img\", { src: url, alt: \"\", className: \"size-full object-cover\" }) : React.createElement(FileIcon, { className: \"size-4\" })\n  )\n}\n"}]}