{
  "generatedFrom": "uploader.contract.json",
  "component": "Upload",
  "slug": "uploader",
  "group": "Data Entry",
  "tier": "leaf-lit",
  "summary": "Select files by clicking a drop zone or dragging onto it — then manage them as rows or picture tiles with per-item state.",
  "install": {
    "package": "@ahaslides-product/design",
    "registry": "https://npm.pkg.github.com",
    "scope": "@ahaslides-product",
    "auth": "Published to GitHub Packages — needs a GitHub token with read:packages. Configure the @ahaslides-product scope in .npmrc before installing.",
    "npmrc": "@ahaslides-product:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}   # a GitHub token with read:packages",
    "command": "npm i @ahaslides-product/design",
    "tokenLayer": "import '@ahaslides-product/design/tokens.css';",
    "import": "import '@ahaslides-product/design/aha-uploader';",
    "element": "aha-uploader",
    "registers": "aha-uploader"
  },
  "feeds": {
    "doc": "https://ahaslides-product.github.io/ahaslides-design/uploader/index.html",
    "md": "https://ahaslides-product.github.io/ahaslides-design/uploader/uploader.md",
    "agentJson": "https://ahaslides-product.github.io/ahaslides-design/uploader.agent.json",
    "llms": "https://ahaslides-product.github.io/ahaslides-design/uploader.llms.txt",
    "index": "https://ahaslides-product.github.io/ahaslides-design/llms.txt"
  },
  "recommendedSnippet": "html",
  "vibeCode": "Emit the HTML snippet by default — <aha-uploader> is a standard custom element that renders on open — React/Vue are thin adapters over the same element. Paste into an .html file and it renders with no build step.",
  "frameworks": {
    "react": {
      "via": "web-component",
      "ref": "aha-uploader"
    },
    "vue": {
      "via": "web-component",
      "ref": "aha-uploader"
    },
    "html": {
      "via": "web-component",
      "ref": "aha-uploader",
      "runnable": "paste-and-run — no build step, renders on open"
    }
  },
  "props": [
    {
      "name": "listtype",
      "type": "'text' | 'picture-card'",
      "default": "text",
      "desc": "Drop zone + file rows, or a grid of thumbnail tiles with a dashed add-box"
    },
    {
      "name": "items",
      "type": "JSON list",
      "default": "[]",
      "desc": "Managed files: [{ name, status?: 'normal'|'uploading'|'error'|'done', percent?, thumb? }]"
    },
    {
      "name": "multiple",
      "type": "boolean",
      "default": "false",
      "desc": "Allow selecting more than one file"
    },
    {
      "name": "accept",
      "type": "string",
      "default": "—",
      "desc": "MIME/type filter, e.g. \"image/*\""
    },
    {
      "name": "maxcount",
      "type": "number",
      "default": "∞",
      "desc": "Cap; the add trigger hides once items reach it"
    },
    {
      "name": "disabled",
      "type": "boolean",
      "default": "false",
      "desc": "Dims and disables the zone / add-box"
    },
    {
      "name": "change",
      "type": "CustomEvent<{files}>",
      "default": "—",
      "desc": "Composed event on select; read e.detail.files (File[])"
    },
    {
      "name": "remove",
      "type": "CustomEvent<{name,index}>",
      "default": "—",
      "desc": "Composed event when an item's ✕ is clicked"
    }
  ],
  "tokens": [
    "bg-container",
    "bg-container-secondary",
    "bg-hover",
    "bg-accent",
    "bg-negative",
    "border",
    "border-strong",
    "color-primary",
    "color-error",
    "color-success",
    "text-default",
    "text-secondary",
    "text-tertiary",
    "split",
    "radius-default",
    "radius-sm",
    "radius-pill",
    "font-product",
    "motion-fast",
    "motion-mid",
    "motion-slow",
    "ease-in-out",
    "ease-out"
  ],
  "spec": [
    {
      "label": "Zone",
      "value": "1px dashed border-strong #D4D4D4, radius 8, pad 24 16"
    },
    {
      "label": "Rest text",
      "value": "text-tertiary #8A8A8A, 14/21"
    },
    {
      "label": "Hover / drag",
      "value": "border + accent tint color-primary / bg-accent (motion-mid, persistent zone)"
    },
    {
      "label": "File row",
      "value": "1px border #E3E3E3, radius 8, file icon + name + remove ✕"
    },
    {
      "label": "Uploading",
      "value": "4px pill progress track (split), color-primary fill, width animates (motion-slow)"
    },
    {
      "label": "Error item",
      "value": "border + text color-error #F5222D on bg-negative, warning glyph"
    },
    {
      "label": "Done item",
      "value": "check-circle glyph color-success #16C49A"
    },
    {
      "label": "Picture-card",
      "value": "104px tiles + dashed add-box; add-box is the trigger"
    }
  ],
  "opinion": {
    "whenToUse": [
      {
        "what": "Upload",
        "when": "the user brings a file in — an image, a CSV, a document"
      },
      {
        "what": "Input",
        "when": "capturing typed text, not a file"
      },
      {
        "what": "Colour picker",
        "when": "choosing a value from presets rather than importing"
      }
    ],
    "note": "State what you accept in the zone copy and set `accept` to match. Drive `items[].status` from your upload lifecycle (uploading → done/error) and update `percent` as bytes land. Show the error reason near the failed row. Use `picture-card` for images, `text` for documents. Offer both click and drag — both are wired here.",
    "dsv3": "COVERED — enumerated from the DS V3 Upload component-set taxonomy: *Upload* / Drag and drop (the drop zone), *Upload* / Upload List Item (text rows, states loading/success/error), Upload List Item / Picture (picture-card tiles + add-box). Token layer: dashed border-strong zone on white → color-primary border + bg-accent tint on hover/drag (persistent zone); rows on 1px border #E3E3E3 radius 8; error = color-error on bg-negative; done = color-success check; uploading = color-primary progress fill on a split track (width animates via motion-slow). Motion bound to the shared motion tokens on persistent nodes."
  },
  "surfaces": [
    "editor",
    "dashboard",
    "settings"
  ],
  "snippets": {
    "html": "<!-- Paste-and-run: save as .html and open in a browser. No build step.\n     <aha-uploader> is the SAME shared custom element React and Vue consume.\n     Click or drag files onto the zone; it emits a composed `change` event with the File[].\n     Feed `items` to render managed file rows/tiles with per-item state + a remove ✕. -->\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css\">\n<script type=\"module\">\n  import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/icons.js';       // <aha-icon> glyphs\n  import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-uploader.js'; // <aha-uploader>\n</script>\n\n<!-- text mode: a drop zone + managed file rows (done / uploading / error) -->\n<aha-uploader id=\"up\" accept=\"image/*\" multiple\n  items='[{\"name\":\"cover.png\",\"status\":\"done\"},{\"name\":\"clip.mov\",\"status\":\"uploading\",\"percent\":40},{\"name\":\"bad.zip\",\"status\":\"error\"}]'>\n</aha-uploader>\n\n<!-- picture-card mode: thumbnail tiles + a dashed add-box; cap with maxcount -->\n<aha-uploader listtype=\"picture-card\" maxcount=\"4\"\n  items='[{\"name\":\"a.png\",\"status\":\"done\",\"thumb\":\"a.png\"}]'>\n</aha-uploader>\n\n<script>\n  const up = document.getElementById('up');\n  up.addEventListener('change', (e) => console.log('files:', e.detail.files.map((f) => f.name)));\n  up.addEventListener('remove', (e) => console.log('remove:', e.detail.name, e.detail.index));\n</script>\n",
    "react": "import '@ahaslides-product/design/aha-uploader';   // registers <aha-uploader>\nimport '@ahaslides-product/design/icons';          // registers <aha-icon> (glyphs)\nimport { useRef, useEffect } from 'react';\n\n// React 18 needs a thin wrapper (composed CustomEvents + JSON attrs); React 19 can use the tag directly.\nfunction AhaUploader({ multiple, accept, disabled, listType, maxCount, items, onFiles, onRemove }) {\n  const ref = useRef();\n  useEffect(() => {\n    const el = ref.current;\n    const f = (e) => onFiles?.(e.detail.files);           // composed CustomEvent, File[]\n    const r = (e) => onRemove?.(e.detail.name, e.detail.index);\n    el.addEventListener('change', f);\n    el.addEventListener('remove', r);\n    return () => { el.removeEventListener('change', f); el.removeEventListener('remove', r); };\n  });\n  return (\n    <aha-uploader\n      ref={ref}\n      multiple={multiple || undefined}\n      accept={accept}\n      disabled={disabled || undefined}\n      listtype={listType}                                 // \"text\" | \"picture-card\"\n      maxcount={maxCount}\n      items={items ? JSON.stringify(items) : undefined}   // [{ name, status?, percent?, thumb? }]\n    />\n  );\n}\n\n// usage — a drop zone with managed rows (done / uploading / error) + a remove handler\n<AhaUploader\n  accept=\"image/*\"\n  multiple\n  items={[\n    { name: 'cover.png', status: 'done' },\n    { name: 'clip.mov', status: 'uploading', percent: 40 },\n    { name: 'bad.zip', status: 'error' },\n  ]}\n  onFiles={(files) => setFiles(files)}\n  onRemove={(name) => dropFile(name)}\n/>\n",
    "vue": "// main.ts — register the element + mark aha-* as custom elements\nimport '@ahaslides-product/design/aha-uploader';\nimport '@ahaslides-product/design/icons';           // <aha-icon> glyphs\napp.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');\n\n// Component.vue — text mode: a drop zone + managed file rows (done / uploading / error)\n<template>\n  <aha-uploader\n    accept=\"image/*\"\n    multiple\n    listtype=\"text\"\n    :items=\"JSON.stringify(items)\"          // [{ name, status?, percent?, thumb? }]\n    @change=\"onFiles($event.detail.files)\"\n    @remove=\"onRemove($event.detail.name, $event.detail.index)\"\n  />\n\n  <!-- picture-card mode: thumbnail tiles + a dashed add-box, capped at 4 -->\n  <aha-uploader listtype=\"picture-card\" maxcount=\"4\" :items=\"JSON.stringify(tiles)\" />\n</template>\n\n<script setup>\nconst items = [\n  { name: 'cover.png', status: 'done' },\n  { name: 'clip.mov', status: 'uploading', percent: 40 },\n  { name: 'bad.zip', status: 'error' },\n];\nconst tiles = [{ name: 'a.png', status: 'done', thumb: 'a.png' }];\n</script>\n"
  }
}
