{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-upload",
  "type": "registry:ui",
  "title": "File Upload",
  "description": "Styled label-wrapped native <input type=\"file\"> with drag-and-drop, a filename/preview list, and a native <progress> bar. Submits via standard multipart POST. Ships in five flavours.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/file-upload.tsx",
      "type": "registry:ui",
      "target": "components/ui/file-upload.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\n\n// File Upload — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// A styled <label>-wrapped native <input type=\"file\"> with an optional\n// drag-and-drop enhancement, a selected-file list (with image previews),\n// and a native <progress> upload bar. It submits via a standard multipart\n// POST — no custom transport — so it works with a plain <form> and degrades\n// to a bare file picker when JavaScript is off.\n//\n// There is no \"file upload\" or \"dropzone\" element in the platform — the\n// pieces are:\n//   - <input type=\"file\"> + <label>  — the picker + its accessible name.\n//       repos/mdn/files/en-us/web/html/reference/elements/input/file/index.md\n//   - Drag-and-Drop API (drop/dragover) + File API (input.files, FileList)\n//     for the optional drop-zone enhancement.\n//       repos/mdn/files/en-us/web/api/html_drag_and_drop_api/file_drag_and_drop/index.md\n//   - htmx multipart upload (hx-encoding=\"multipart/form-data\") and\n//     hx-preserve to keep the selection across re-render-on-error swaps.\n//       repos/htmx/www/src/content/patterns/02-forms/03-file-upload.md\n//       repos/htmx/www/reference.md  (hx-encoding, hx-preserve)\n//\n// Style analogues (matched exactly): registry/ui/input.tsx (the field /\n// file:* affordance + focus-visible ring + .htmx-request dim) and\n// registry/ui/progress.tsx (the native <progress> visual).\n//\n// The keyboard/behaviour contract (drop wiring + filename/preview list +\n// reset) lives in public/site.js, scoped to [data-slot=\"file-upload\"]; an\n// inline boot script next to the root only marks it ready, so a server swap\n// re-arms cleanly. None of it is required for the upload to work.\n\nconst root =\n  \"group/file-upload grid w-full gap-3 \" +\n  // While a request triggered by/targeting this control is in flight,\n  // htmx adds .htmx-request — dim like Input does.\n  \"[&.htmx-request]:opacity-70\"\n\nconst zone =\n  \"flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-transparent px-6 py-8 text-center text-sm text-muted-foreground shadow-xs transition-[color,box-shadow] outline-none \" +\n  \"dark:bg-input/30 \" +\n  // The visually-hidden <input> is the real focus target; mirror its focus\n  // ring onto the styled label via :focus-within.\n  \"focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 \" +\n  \"hover:border-ring/60 hover:text-foreground \" +\n  // site.js sets data-dragover on the label while a file is over it.\n  \"data-[dragover=true]:border-ring data-[dragover=true]:bg-accent data-[dragover=true]:text-foreground \" +\n  // Disabled mirrors Input.\n  \"has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50 \" +\n  \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\"\n\n// Visually-hidden but still focusable + operable: the native picker stays\n// the accessible control; the label is its visible skin.\nconst srOnly =\n  \"absolute size-px overflow-hidden border-0 p-0 whitespace-nowrap [clip:rect(0,0,0,0)]\"\n\ntype FileUploadProps = {\n  // Form field name — required to actually submit. multiple sends one entry\n  // per file under this name.\n  name?: string\n  id?: string\n  // Comma-separated unique file type specifiers (\".pdf,image/*\"). Native\n  // filter in the OS picker; the drop enhancement re-checks it too.\n  accept?: string\n  multiple?: boolean\n  required?: boolean\n  disabled?: boolean\n  // type=\"file\" only — request the OS camera (user | environment).\n  capture?: \"user\" | \"environment\" | boolean\n  // Associate the input with a <form> by id. Per the htmx pattern, putting\n  // the file input OUTSIDE the swap target (via form=) is an alternative to\n  // hx-preserve for keeping the selection across error re-renders.\n  form?: string\n  // htmx: keep the chosen file across an outerHTML/innerHTML swap when the\n  // form re-renders with validation errors. Renders the hx-preserve attr.\n  preserve?: boolean\n\n  // Visible prompt + sub-label inside the drop zone.\n  label?: string\n  hint?: string\n\n  // Show the native <progress> bar. value=undefined → indeterminate\n  // (\"uploading…, length unknown\"); a number 0–100 → determinate.\n  showProgress?: boolean\n  progress?: number\n\n  class?: ClassValue\n  // ARIA — the visible label text names the input by default; override here\n  // when there is no visible label or a separate one elsewhere.\n  ariaLabel?: string\n  ariaLabelledby?: string\n  ariaDescribedby?: string\n  ariaInvalid?: boolean | \"grammar\" | \"spelling\"\n\n  // htmx v4 (subset). Usually set on the wrapping <form> (hx-post +\n  // hx-encoding=\"multipart/form-data\"), but forwarded here too so a\n  // standalone control can drive an upload on change.\n  \"hx-post\"?: string\n  \"hx-put\"?: string\n  \"hx-target\"?: string\n  \"hx-swap\"?: string\n  \"hx-trigger\"?: string\n  \"hx-encoding\"?: string\n  \"hx-indicator\"?: string\n  \"hx-include\"?: string\n  \"hx-preserve\"?: boolean | \"true\"\n  \"hx-disable\"?: string\n}\n\nexport function FileUpload(props: FileUploadProps) {\n  const {\n    name,\n    id,\n    accept,\n    multiple,\n    required,\n    disabled,\n    capture,\n    form,\n    preserve,\n    label = \"Drop files here, or click to upload\",\n    hint,\n    showProgress,\n    progress,\n    class: className,\n    ariaLabel,\n    ariaLabelledby,\n    ariaDescribedby,\n    ariaInvalid,\n    ...rest\n  } = props\n\n  const determinate = progress !== undefined\n  const pct = determinate ? Math.min(100, Math.max(0, progress!)) : 0\n\n  // Boot marks the root ready so site.js arms drop + the file list once,\n  // and re-arms after an htmx swap re-inserts a fresh root.\n  const boot = `(function(el){el.setAttribute('data-file-upload-ready','true');})(document.currentScript.previousElementSibling);`\n\n  return (\n    <>\n      <div\n        id={id}\n        data-slot=\"file-upload\"\n        class={cn(root, className)}\n      >\n        <label\n          data-slot=\"file-upload-zone\"\n          aria-invalid={ariaInvalid === undefined ? undefined : String(ariaInvalid)}\n          class={zone}\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"size-6 shrink-0 opacity-70\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M12 13v8\" />\n            <path d=\"m8 17 4-4 4 4\" />\n            <path d=\"M20 16.7A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25\" />\n          </svg>\n          <span class=\"font-medium text-foreground\">{label}</span>\n          {hint && <span class=\"text-xs\">{hint}</span>}\n          <input\n            type=\"file\"\n            data-slot=\"file-upload-input\"\n            class={srOnly}\n            name={name}\n            accept={accept}\n            multiple={multiple}\n            required={required}\n            disabled={disabled}\n            capture={capture}\n            form={form}\n            hx-preserve={preserve ? \"true\" : undefined}\n            aria-label={ariaLabel ?? (ariaLabelledby ? undefined : label)}\n            aria-labelledby={ariaLabelledby}\n            aria-describedby={ariaDescribedby}\n            aria-invalid={ariaInvalid === undefined ? undefined : String(ariaInvalid)}\n            {...rest}\n          />\n        </label>\n\n        <ul\n          data-slot=\"file-upload-list\"\n          class=\"m-0 grid list-none gap-2 p-0 empty:hidden\"\n          aria-live=\"polite\"\n        />\n\n        {showProgress && (\n          <div\n            data-slot=\"file-upload-progress\"\n            role=\"progressbar\"\n            aria-label=\"Upload progress\"\n            aria-valuemin={0}\n            aria-valuemax={100}\n            aria-valuenow={determinate ? pct : undefined}\n            data-state={determinate ? \"determinate\" : \"indeterminate\"}\n            class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\"\n          >\n            <div\n              data-slot=\"file-upload-progress-indicator\"\n              class={cn(\n                \"h-full bg-primary transition-all\",\n                !determinate &&\n                  \"absolute inset-y-0 -left-1/3 w-1/3 animate-[scn-progress-indeterminate_1.2s_ease-in-out_infinite]\",\n              )}\n              style={determinate ? `width: ${pct}%` : undefined}\n            />\n          </div>\n        )}\n      </div>\n      <script\n        // biome-ignore lint/security/noDangerouslySetInnerHtml: SSR boot\n        dangerouslySetInnerHTML={{ __html: boot }}\n      />\n    </>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/file-upload.html",
      "type": "registry:file",
      "target": "templates/components/file-upload.html",
      "content": "{# File Upload macro — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/file-upload.tsx for Python/Flask/FastAPI/Django/Jinja2.\n\n   A <label>-wrapped native <input type=\"file\"> with a drag-and-drop\n   enhancement, a selected-file list, and a native <progress> bar. Submits\n   via a standard multipart POST.\n\n   Usage:\n       {% from \"components/file-upload.html\" import file_upload %}\n\n       <form hx-post=\"/upload\" hx-encoding=\"multipart/form-data\"\n             hx-target=\"#result\" hx-swap=\"outerHTML\">\n         {{ file_upload(name=\"files\", accept=\"image/*\", multiple=true,\n                        hint=\"PNG, JPG up to 5MB\", show_progress=true,\n                        preserve=true) }}\n         <button type=\"submit\">Upload</button>\n       </form>\n\n   Sources (see header of file-upload.tsx for the full citations):\n     repos/mdn/.../elements/input/file\n     repos/mdn/.../api/html_drag_and_drop_api/file_drag_and_drop\n     repos/htmx/.../patterns/02-forms/03-file-upload (hx-encoding, hx-preserve)\n\n   All hx-* / data-* / aria-* pass through via **attrs (underscores become\n   dashes, so `hx_post=\"/upload\"` emits `hx-post=\"/upload\"`). #}\n\n{% macro file_upload(\n    name=none,\n    id=none,\n    accept=none,\n    multiple=false,\n    required=false,\n    disabled=false,\n    capture=none,\n    form=none,\n    preserve=false,\n    label=\"Drop files here, or click to upload\",\n    hint=none,\n    show_progress=false,\n    progress=none,\n    aria_label=none,\n    aria_labelledby=none,\n    aria_describedby=none,\n    aria_invalid=none,\n    extra_class=\"\",\n    **attrs\n) %}\n{%- set root -%}\ngroup/file-upload grid w-full gap-3 [&.htmx-request]:opacity-70\n{%- endset -%}\n{%- set zone -%}\nflex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-transparent px-6 py-8 text-center text-sm text-muted-foreground shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30 focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 hover:border-ring/60 hover:text-foreground data-[dragover=true]:border-ring data-[dragover=true]:bg-accent data-[dragover=true]:text-foreground has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\n{%- endset -%}\n{%- set sr_only -%}\nabsolute size-px overflow-hidden border-0 p-0 whitespace-nowrap [clip:rect(0,0,0,0)]\n{%- endset -%}\n{%- set determinate = progress is not none -%}\n{%- set pct = ([progress|float, 0]|max) if determinate else 0 -%}\n{%- set pct = [pct, 100]|min if determinate else 0 -%}\n<div data-slot=\"file-upload\" class=\"{{ root }} {{ extra_class }}\"\n     {%- if id %} id=\"{{ id }}\"{% endif %}>\n  <label data-slot=\"file-upload-zone\"\n         {%- if aria_invalid is not none %} aria-invalid=\"{{ aria_invalid|string|lower }}\"{% endif %}\n         class=\"{{ zone }}\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6 shrink-0 opacity-70\" aria-hidden=\"true\">\n      <path d=\"M12 13v8\" />\n      <path d=\"m8 17 4-4 4 4\" />\n      <path d=\"M20 16.7A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25\" />\n    </svg>\n    <span class=\"font-medium text-foreground\">{{ label }}</span>\n    {%- if hint %}<span class=\"text-xs\">{{ hint }}</span>{% endif %}\n    <input type=\"file\" data-slot=\"file-upload-input\" class=\"{{ sr_only }}\"\n           {%- if name %} name=\"{{ name }}\"{% endif %}\n           {%- if accept %} accept=\"{{ accept }}\"{% endif %}\n           {%- if multiple %} multiple{% endif %}\n           {%- if required %} required{% endif %}\n           {%- if disabled %} disabled{% endif %}\n           {%- if capture is not none %} capture{% if capture is string %}=\"{{ capture }}\"{% endif %}{% endif %}\n           {%- if form %} form=\"{{ form }}\"{% endif %}\n           {%- if preserve %} hx-preserve=\"true\"{% endif %}\n           {%- if aria_label %} aria-label=\"{{ aria_label }}\"{% elif not aria_labelledby %} aria-label=\"{{ label }}\"{% endif %}\n           {%- if aria_labelledby %} aria-labelledby=\"{{ aria_labelledby }}\"{% endif %}\n           {%- if aria_describedby %} aria-describedby=\"{{ aria_describedby }}\"{% endif %}\n           {%- if aria_invalid is not none %} aria-invalid=\"{{ aria_invalid|string|lower }}\"{% endif %}\n           {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n    >\n  </label>\n\n  <ul data-slot=\"file-upload-list\" class=\"m-0 grid list-none gap-2 p-0 empty:hidden\" aria-live=\"polite\"></ul>\n\n  {%- if show_progress %}\n  <div data-slot=\"file-upload-progress\" role=\"progressbar\" aria-label=\"Upload progress\"\n       aria-valuemin=\"0\" aria-valuemax=\"100\"\n       {%- if determinate %} aria-valuenow=\"{{ pct }}\"{% endif %}\n       data-state=\"{{ 'determinate' if determinate else 'indeterminate' }}\"\n       class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\">\n    <div data-slot=\"file-upload-progress-indicator\"\n         class=\"h-full bg-primary transition-all{% if not determinate %} absolute inset-y-0 -left-1/3 w-1/3 animate-[scn-progress-indeterminate_1.2s_ease-in-out_infinite]{% endif %}\"\n         {%- if determinate %} style=\"width: {{ pct }}%\"{% endif %}></div>\n  </div>\n  {%- endif %}\n</div>\n<script>(function(el){el.setAttribute('data-file-upload-ready','true');})(document.currentScript.previousElementSibling);</script>\n{% endmacro %}\n"
    },
    {
      "path": "registry/go-templates/file-upload.tmpl",
      "type": "registry:file",
      "target": "components/file-upload.tmpl",
      "content": "{{/*\n  File Upload template — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/file-upload.tsx for Go projects using html/template.\n\n  A <label>-wrapped native <input type=\"file\"> with a drag-and-drop\n  enhancement, a selected-file list, and a native <progress> bar. Submits via\n  a standard multipart POST.\n\n  Usage in your code:\n\n      type FileUploadArgs struct {\n          Name        string\n          ID          string\n          Accept      string // \".pdf,image/*\"\n          Multiple    bool\n          Required    bool\n          Disabled    bool\n          Capture     string // \"\" | \"user\" | \"environment\" | \"true\"\n          Form        string // associate with a <form> by id\n          Preserve    bool   // emit hx-preserve=\"true\"\n          Label       string // visible prompt (default below)\n          Hint        string // sub-label\n          ShowProgress bool\n          Progress    string // \"\" = indeterminate, \"0\"..\"100\" = determinate\n\n          AriaLabel       string\n          AriaLabelledby  string\n          AriaDescribedby string\n          AriaInvalid     string // \"true\" | \"false\" | \"grammar\" | \"spelling\"\n\n          // Everything else (hx-post, hx-encoding, hx-target, …)\n          Attrs map[string]string\n      }\n\n      tpl.ExecuteTemplate(w, \"file-upload\", FileUploadArgs{\n          Name: \"files\", Accept: \"image/*\", Multiple: true,\n          Hint: \"PNG, JPG up to 5MB\", ShowProgress: true, Preserve: true,\n      })\n\n  Sources: repos/mdn/.../elements/input/file,\n  repos/mdn/.../api/html_drag_and_drop_api/file_drag_and_drop,\n  repos/htmx/.../patterns/02-forms/03-file-upload (hx-encoding, hx-preserve).\n*/}}\n\n{{define \"file-upload\"}}\n{{- $label := or .Label \"Drop files here, or click to upload\" -}}\n{{- $determinate := ne .Progress \"\" -}}\n{{- $root := \"group/file-upload grid w-full gap-3 [&.htmx-request]:opacity-70\" -}}\n{{- $zone := \"flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-transparent px-6 py-8 text-center text-sm text-muted-foreground shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30 focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 hover:border-ring/60 hover:text-foreground data-[dragover=true]:border-ring data-[dragover=true]:bg-accent data-[dragover=true]:text-foreground has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\" -}}\n{{- $srOnly := \"absolute size-px overflow-hidden border-0 p-0 whitespace-nowrap [clip:rect(0,0,0,0)]\" -}}\n<div data-slot=\"file-upload\" class=\"{{$root}}\"\n     {{- if .ID}} id=\"{{.ID}}\"{{end}}>\n  <label data-slot=\"file-upload-zone\"\n         {{- if .AriaInvalid}} aria-invalid=\"{{.AriaInvalid}}\"{{end}}\n         class=\"{{$zone}}\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6 shrink-0 opacity-70\" aria-hidden=\"true\">\n      <path d=\"M12 13v8\" />\n      <path d=\"m8 17 4-4 4 4\" />\n      <path d=\"M20 16.7A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25\" />\n    </svg>\n    <span class=\"font-medium text-foreground\">{{$label}}</span>\n    {{- if .Hint}}<span class=\"text-xs\">{{.Hint}}</span>{{end}}\n    <input type=\"file\" data-slot=\"file-upload-input\" class=\"{{$srOnly}}\"\n           {{- if .Name}} name=\"{{.Name}}\"{{end}}\n           {{- if .Accept}} accept=\"{{.Accept}}\"{{end}}\n           {{- if .Multiple}} multiple{{end}}\n           {{- if .Required}} required{{end}}\n           {{- if .Disabled}} disabled{{end}}\n           {{- if .Capture}} capture{{if ne .Capture \"true\"}}=\"{{.Capture}}\"{{end}}{{end}}\n           {{- if .Form}} form=\"{{.Form}}\"{{end}}\n           {{- if .Preserve}} hx-preserve=\"true\"{{end}}\n           {{- if .AriaLabel}} aria-label=\"{{.AriaLabel}}\"{{else if not .AriaLabelledby}} aria-label=\"{{$label}}\"{{end}}\n           {{- if .AriaLabelledby}} aria-labelledby=\"{{.AriaLabelledby}}\"{{end}}\n           {{- if .AriaDescribedby}} aria-describedby=\"{{.AriaDescribedby}}\"{{end}}\n           {{- if .AriaInvalid}} aria-invalid=\"{{.AriaInvalid}}\"{{end}}\n           {{- range $k, $v := .Attrs}} {{$k}}=\"{{$v}}\"{{end -}}\n    >\n  </label>\n\n  <ul data-slot=\"file-upload-list\" class=\"m-0 grid list-none gap-2 p-0 empty:hidden\" aria-live=\"polite\"></ul>\n\n  {{- if .ShowProgress}}\n  <div data-slot=\"file-upload-progress\" role=\"progressbar\" aria-label=\"Upload progress\"\n       aria-valuemin=\"0\" aria-valuemax=\"100\"\n       {{- if $determinate}} aria-valuenow=\"{{.Progress}}\"{{end}}\n       data-state=\"{{if $determinate}}determinate{{else}}indeterminate{{end}}\"\n       class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\">\n    <div data-slot=\"file-upload-progress-indicator\"\n         class=\"h-full bg-primary transition-all{{if not $determinate}} absolute inset-y-0 -left-1/3 w-1/3 animate-[scn-progress-indeterminate_1.2s_ease-in-out_infinite]{{end}}\"\n         {{- if $determinate}} style=\"width: {{.Progress}}%\"{{end}}></div>\n  </div>\n  {{- end}}\n</div>\n<script>(function(el){el.setAttribute('data-file-upload-ready','true');})(document.currentScript.previousElementSibling);</script>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/file_upload.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/file_upload.ex",
      "content": "defmodule ShadcnHtmx.Components.FileUpload do\n  @moduledoc \"\"\"\n  File Upload — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  Mirrors registry/ui/file-upload.tsx. A `<label>`-wrapped native\n  `<input type=\"file\">` with a drag-and-drop enhancement, a selected-file\n  list, and a native `<progress>` bar. Submits via a standard multipart POST.\n\n  The drop wiring + filename/preview list live in public/site.js, scoped to\n  `[data-slot=\"file-upload\"]`. An inline boot script marks the root ready so\n  a swap re-arms cleanly. None of it is required for the upload to work.\n\n  ## Examples\n\n      <form hx-post=\"/upload\" hx-encoding=\"multipart/form-data\"\n            hx-target=\"#result\" hx-swap=\"outerHTML\">\n        <.file_upload name=\"files\" accept=\"image/*\" multiple\n          hint=\"PNG, JPG up to 5MB\" show_progress preserve />\n        <button type=\"submit\">Upload</button>\n      </form>\n\n  Sources: repos/mdn/.../elements/input/file,\n  repos/mdn/.../api/html_drag_and_drop_api/file_drag_and_drop,\n  repos/htmx/.../patterns/02-forms/03-file-upload (hx-encoding, hx-preserve).\n  \"\"\"\n\n  use Phoenix.Component\n\n  @root \"group/file-upload grid w-full gap-3 [&.htmx-request]:opacity-70\"\n\n  @zone \"flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input \" <>\n          \"bg-transparent px-6 py-8 text-center text-sm text-muted-foreground shadow-xs transition-[color,box-shadow] outline-none \" <>\n          \"dark:bg-input/30 \" <>\n          \"focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 \" <>\n          \"hover:border-ring/60 hover:text-foreground \" <>\n          \"data-[dragover=true]:border-ring data-[dragover=true]:bg-accent data-[dragover=true]:text-foreground \" <>\n          \"has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50 \" <>\n          \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\"\n\n  @sr_only \"absolute size-px overflow-hidden border-0 p-0 whitespace-nowrap [clip:rect(0,0,0,0)]\"\n\n  attr :name, :string, default: nil\n  attr :id, :string, default: nil\n  attr :accept, :string, default: nil\n  attr :multiple, :boolean, default: false\n  attr :required, :boolean, default: false\n  attr :disabled, :boolean, default: false\n  attr :capture, :string, default: nil\n  attr :form, :string, default: nil\n  attr :preserve, :boolean, default: false\n  attr :label, :string, default: \"Drop files here, or click to upload\"\n  attr :hint, :string, default: nil\n  attr :show_progress, :boolean, default: false\n  attr :progress, :integer, default: nil\n  attr :class, :string, default: nil\n\n  attr :rest, :global,\n    include:\n      ~w(hx-post hx-put hx-target hx-swap hx-trigger hx-encoding hx-indicator hx-include hx-disable\n         aria-label aria-labelledby aria-describedby aria-invalid)\n\n  def file_upload(assigns) do\n    determinate = assigns.progress != nil\n    pct = if determinate, do: assigns.progress |> max(0) |> min(100), else: 0\n\n    assigns =\n      assigns\n      |> assign(:root_class, @root)\n      |> assign(:zone_class, @zone)\n      |> assign(:sr_only_class, @sr_only)\n      |> assign(:determinate, determinate)\n      |> assign(:pct, pct)\n\n    ~H\"\"\"\n    <div id={@id} data-slot=\"file-upload\" class={[@root_class, @class]}>\n      <label data-slot=\"file-upload-zone\" class={@zone_class}>\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          class=\"size-6 shrink-0 opacity-70\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M12 13v8\" />\n          <path d=\"m8 17 4-4 4 4\" />\n          <path d=\"M20 16.7A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25\" />\n        </svg>\n        <span class=\"font-medium text-foreground\">{@label}</span>\n        <span :if={@hint} class=\"text-xs\">{@hint}</span>\n        <input\n          type=\"file\"\n          data-slot=\"file-upload-input\"\n          class={@sr_only_class}\n          name={@name}\n          accept={@accept}\n          multiple={@multiple}\n          required={@required}\n          disabled={@disabled}\n          capture={@capture}\n          form={@form}\n          hx-preserve={if @preserve, do: \"true\"}\n          aria-label={@label}\n          {@rest}\n        />\n      </label>\n\n      <ul\n        data-slot=\"file-upload-list\"\n        class=\"m-0 grid list-none gap-2 p-0 empty:hidden\"\n        aria-live=\"polite\"\n      >\n      </ul>\n\n      <div\n        :if={@show_progress}\n        data-slot=\"file-upload-progress\"\n        role=\"progressbar\"\n        aria-label=\"Upload progress\"\n        aria-valuemin=\"0\"\n        aria-valuemax=\"100\"\n        aria-valuenow={if @determinate, do: @pct}\n        data-state={if @determinate, do: \"determinate\", else: \"indeterminate\"}\n        class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\"\n      >\n        <div\n          data-slot=\"file-upload-progress-indicator\"\n          class={[\n            \"h-full bg-primary transition-all\",\n            !@determinate &&\n              \"absolute inset-y-0 -left-1/3 w-1/3 animate-[scn-progress-indeterminate_1.2s_ease-in-out_infinite]\"\n          ]}\n          style={if @determinate, do: \"width: #{@pct}%\"}\n        >\n        </div>\n      </div>\n    </div>\n    <script>{Phoenix.HTML.raw(~s\"\"\"\n      (function(el){el.setAttribute('data-file-upload-ready','true');})(document.currentScript.previousElementSibling);\n    \"\"\")}</script>\n    \"\"\"\n  end\nend\n"
    },
    {
      "path": "registry/html/file-upload.html",
      "type": "registry:file",
      "target": "snippets/file-upload.html",
      "content": "<!--\n  shadcn-htmx — raw HTML file-upload snippet.\n\n  Mirrors registry/ui/file-upload.tsx. Drop onto any page that loads Tailwind\n  CSS v4 + the shadcn theme variables (input, ring, accent, foreground,\n  muted-foreground, primary) and htmx. See app/styles/input.css for the\n  variable defaults and the scn-progress-indeterminate keyframes.\n\n  A <label>-wrapped native <input type=\"file\"> with a drag-and-drop\n  enhancement, a selected-file list, and a native <progress> bar. Submits via\n  a standard multipart POST — wrap it in a <form> with:\n\n      <form hx-post=\"/upload\" hx-encoding=\"multipart/form-data\"\n            hx-target=\"#result\" hx-swap=\"outerHTML\"> … </form>\n\n  The drop wiring + filename/preview list come from public/site.js\n  ([data-slot=\"file-upload\"]); the inline boot <script> just marks the root\n  ready so a swap re-arms. Without JS this is a plain file picker that still\n  uploads.\n\n  Sources: repos/mdn/.../elements/input/file,\n  repos/mdn/.../api/html_drag_and_drop_api/file_drag_and_drop,\n  repos/htmx/.../patterns/02-forms/03-file-upload (hx-encoding, hx-preserve).\n-->\n\n<form hx-post=\"/upload\" hx-encoding=\"multipart/form-data\" hx-target=\"#fu-result\" hx-swap=\"outerHTML\">\n  <div data-slot=\"file-upload\" class=\"group/file-upload grid w-full gap-3 [&.htmx-request]:opacity-70\">\n    <label data-slot=\"file-upload-zone\"\n      class=\"flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-transparent px-6 py-8 text-center text-sm text-muted-foreground shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30 focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 hover:border-ring/60 hover:text-foreground data-[dragover=true]:border-ring data-[dragover=true]:bg-accent data-[dragover=true]:text-foreground has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\">\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6 shrink-0 opacity-70\" aria-hidden=\"true\">\n        <path d=\"M12 13v8\" />\n        <path d=\"m8 17 4-4 4 4\" />\n        <path d=\"M20 16.7A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25\" />\n      </svg>\n      <span class=\"font-medium text-foreground\">Drop files here, or click to upload</span>\n      <span class=\"text-xs\">PNG, JPG up to 5MB</span>\n      <input type=\"file\" data-slot=\"file-upload-input\"\n        class=\"absolute size-px overflow-hidden border-0 p-0 whitespace-nowrap [clip:rect(0,0,0,0)]\"\n        name=\"files\" accept=\"image/*\" multiple hx-preserve=\"true\"\n        aria-label=\"Drop files here, or click to upload\">\n    </label>\n\n    <ul data-slot=\"file-upload-list\" class=\"m-0 grid list-none gap-2 p-0 empty:hidden\" aria-live=\"polite\"></ul>\n\n    <div data-slot=\"file-upload-progress\" role=\"progressbar\" aria-label=\"Upload progress\"\n      aria-valuemin=\"0\" aria-valuemax=\"100\" data-state=\"indeterminate\"\n      class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\">\n      <div data-slot=\"file-upload-progress-indicator\"\n        class=\"h-full bg-primary transition-all absolute inset-y-0 -left-1/3 w-1/3 animate-[scn-progress-indeterminate_1.2s_ease-in-out_infinite]\"></div>\n    </div>\n  </div>\n  <script>(function(el){el.setAttribute('data-file-upload-ready','true');})(document.currentScript.previousElementSibling);</script>\n\n  <button type=\"submit\" class=\"mt-3 inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90\">Upload</button>\n</form>\n"
    }
  ]
}
