{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upload-morph-button",
  "title": "Morphing Upload Button",
  "description": "A button that morphs into its own progress bar, then a check mark.",
  "dependencies": [
    "@uploadcn/core@^0.1.0",
    "@uploadcn/react@^0.1.0",
    "cn",
    "motion"
  ],
  "registryDependencies": [
    "@uploadcn/upload",
    "button"
  ],
  "files": [
    {
      "path": "registry/default/blocks/upload-morph-button.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  type UploadAdapter,\n  type Uploader,\n  type UploadItem as UploadItemData,\n  type ValidationOptions,\n} from \"@uploadcn/core\"\nimport {\n  Upload as UploadPrimitive,\n  useUploadContext,\n  useUploadProgress,\n  type UploadRootProps,\n} from \"@uploadcn/react\"\nimport { cn } from \"cn\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { Upload } from \"@/registry/default/ui/upload\"\n\ninterface UploadMorphButtonProps<TResult> extends Pick<\n  ValidationOptions,\n  \"accept\" | \"maxSize\" | \"maxFiles\"\n> {\n  adapter?: UploadAdapter<TResult>\n  /** Use an uploader you created with `useUploader` instead of options. */\n  uploader?: Uploader<TResult>\n  children?: React.ReactNode\n  multiple?: boolean\n  onSuccess?: (item: UploadItemData<TResult>) => void\n  onComplete?: (items: UploadItemData<TResult>[]) => void\n  className?: string\n}\n\n/**\n * A button that morphs into its own progress bar while uploading, then\n * into a check mark, no extra UI needed.\n */\nfunction UploadMorphButton<TResult = unknown>({\n  adapter,\n  uploader,\n  children = \"Upload\",\n  className,\n  ...options\n}: UploadMorphButtonProps<TResult>) {\n  return (\n    <Upload\n      {...((uploader\n        ? { uploader }\n        : {\n            adapter,\n            ...options,\n          }) as UploadRootProps<TResult>)}\n      className={cn(\"w-auto items-start\", className)}\n    >\n      <MorphTrigger label={children} />\n    </Upload>\n  )\n}\n\ntype Phase = \"idle\" | \"uploading\" | \"done\" | \"error\"\n\nfunction MorphTrigger({ label }: { label: React.ReactNode }) {\n  const { uploader } = useUploadContext()\n  const summary = useUploadProgress(uploader)\n  const reduceMotion = useReducedMotion()\n  const [showDone, setShowDone] = React.useState(false)\n\n  React.useEffect(() => {\n    let timer: ReturnType<typeof setTimeout> | undefined\n    const unsubscribe = uploader.on(\"complete\", ({ items }) => {\n      if (!items.some((item) => item.status === \"success\")) return\n      setShowDone(true)\n      clearTimeout(timer)\n      timer = setTimeout(() => setShowDone(false), 2200)\n    })\n    return () => {\n      unsubscribe()\n      clearTimeout(timer)\n    }\n  }, [uploader])\n\n  const phase: Phase = summary.isUploading\n    ? \"uploading\"\n    : showDone\n      ? \"done\"\n      : summary.hasErrors\n        ? \"error\"\n        : \"idle\"\n\n  const spring = reduceMotion\n    ? { duration: 0 }\n    : { type: \"spring\" as const, bounce: 0.25, duration: 0.5 }\n\n  return (\n    <UploadPrimitive.Trigger\n      data-slot=\"upload-morph-button\"\n      data-phase={phase}\n      disabled={phase === \"uploading\"}\n      aria-live=\"polite\"\n      render={\n        <motion.button\n          layout\n          transition={spring}\n          style={{ borderRadius: 999 }}\n          className={cn(\n            buttonVariants({ size: \"lg\" }),\n            \"relative h-10 overflow-hidden px-5 disabled:opacity-100\",\n            phase === \"done\" && \"w-10 px-0\",\n            phase === \"error\" &&\n              \"bg-destructive text-white hover:bg-destructive/90\"\n          )}\n        />\n      }\n    >\n      {phase === \"uploading\" ? (\n        <motion.span\n          aria-hidden\n          className=\"absolute inset-y-0 left-0 bg-primary-foreground/15\"\n          initial={{ width: 0 }}\n          animate={{ width: `${summary.percent}%` }}\n          transition={{ ease: \"easeOut\", duration: 0.3 }}\n        />\n      ) : null}\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={phase}\n          className=\"relative flex items-center gap-2 whitespace-nowrap\"\n          initial={\n            reduceMotion ? false : { opacity: 0, y: 8, filter: \"blur(4px)\" }\n          }\n          animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n          exit={\n            reduceMotion\n              ? undefined\n              : { opacity: 0, y: -8, filter: \"blur(4px)\" }\n          }\n          transition={{ duration: 0.2 }}\n        >\n          {phase === \"idle\" ? (\n            <>\n              <IconPlaceholder\n                lucide=\"UploadIcon\"\n                tabler=\"IconUpload\"\n                hugeicons=\"Upload01Icon\"\n                phosphor=\"UploadSimpleIcon\"\n                remixicon=\"RiUploadLine\"\n                data-icon=\"inline-start\"\n              />\n              {label}\n            </>\n          ) : phase === \"uploading\" ? (\n            <span className=\"tabular-nums\">\n              Uploading {Math.round(summary.percent)}%\n            </span>\n          ) : phase === \"done\" ? (\n            <DrawnCheck label=\"Uploaded\" />\n          ) : (\n            <>\n              <IconPlaceholder\n                lucide=\"CircleAlertIcon\"\n                tabler=\"IconExclamationCircle\"\n                hugeicons=\"AlertCircleIcon\"\n                phosphor=\"WarningCircleIcon\"\n                remixicon=\"RiErrorWarningLine\"\n                data-icon=\"inline-start\"\n              />\n              Failed, try again\n            </>\n          )}\n        </motion.span>\n      </AnimatePresence>\n    </UploadPrimitive.Trigger>\n  )\n}\n\nfunction DrawnCheck({ label }: { label: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      className=\"size-5\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      role=\"img\"\n      aria-label={label}\n    >\n      <motion.path\n        d=\"M5 12.5l4.5 4.5L19 7.5\"\n        initial={{ pathLength: 0 }}\n        animate={{ pathLength: 1 }}\n        transition={{ duration: 0.35, delay: 0.1, ease: \"easeOut\" }}\n      />\n    </svg>\n  )\n}\n\nexport { UploadMorphButton, type UploadMorphButtonProps }\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "upload",
    "animated"
  ],
  "type": "registry:block"
}