{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-input-motion",
  "title": "Input-motion file",
  "description": "Styled file: the Input-motion set. 5 decorative file variations (DragFile, PopFile, FadeFile, SlideFile, PulseFile) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/file-input-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { File as FileIcon, Upload, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion file-input family: 5 decorative pickers separated by their drag and entry motion. Every export wraps a real <input type=\"file\"> and handles real onDragOver/onDragLeave/onDrop (with preventDefault). Every animation is turned off with useReducedMotion; under reduced motion the state changes instantly instead of transforming. Colour comes ONLY from semantic tokens, transparency through color-mix. Deterministic (no Date.now and no Math.random). */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype FileInputProps = {\n  className?: string\n  size?: StyledSize\n  multiple?: boolean\n  accept?: string\n  onFilesChange?: (files: File[]) => void\n}\n\nfunction fileKey(file: File): string {\n  return `${file.name}:${file.size}:${file.lastModified}`\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`\n  return `${Math.round(bytes / (1024 * 1024))} MB`\n}\n\nfunction useFiles(multiple: boolean | undefined, onFilesChange?: (files: File[]) => void) {\n  const [files, setFiles] = React.useState<File[]>([])\n\n  const add = React.useCallback(\n    (incoming: FileList | File[] | null) => {\n      if (!incoming) return\n      const list = Array.from(incoming)\n      if (list.length === 0) return\n      setFiles((prev) => {\n        if (!multiple) {\n          const next = [list[0]]\n          onFilesChange?.(next)\n          return next\n        }\n        const map = new Map(prev.map((f) => [fileKey(f), f]))\n        for (const f of list) map.set(fileKey(f), f)\n        const next = Array.from(map.values())\n        onFilesChange?.(next)\n        return next\n      })\n    },\n    [multiple, onFilesChange]\n  )\n\n  const remove = React.useCallback(\n    (key: string) => {\n      setFiles((prev) => {\n        const next = prev.filter((f) => fileKey(f) !== key)\n        onFilesChange?.(next)\n        return next\n      })\n    },\n    [onFilesChange]\n  )\n\n  return { files, add, remove }\n}\n\nconst dropSurface: Record<StyledSize, string> = {\n  sm: \"gap-2 rounded-lg p-4 text-sm\",\n  md: \"gap-3 rounded-xl p-6 text-sm\",\n  lg: \"gap-3 rounded-2xl p-8 text-base\",\n  xl: \"gap-4 rounded-2xl p-10 text-base\",\n}\n\nconst iconSize: Record<StyledSize, string> = {\n  sm: \"size-5\",\n  md: \"size-6\",\n  lg: \"size-8\",\n  xl: \"size-9\",\n}\n\nconst clearBtn =\n  \"relative shrink-0 rounded-md text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 after:absolute after:-inset-1.5 motion-reduce:transition-none\"\n\nconst surfaceBase =\n  \"flex w-full flex-col items-center justify-center border border-dashed border-border bg-transparent text-center text-muted-foreground outline-none transition-colors duration-(--motion-base) ease-(--motion-ease) hover:border-primary/60 focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[drag-active]:border-primary data-[drag-active]:bg-[color-mix(in_oklab,var(--color-primary)_10%,transparent)] data-[drag-active]:text-foreground motion-reduce:transition-none [&>svg]:shrink-0 [&>i]:leading-none\"\n\nconst springy = { type: \"spring\" as const, stiffness: 420, damping: 30 }\n\n/* Ortak dosya listesi. Hareket tipi disaridan verilir. */\nfunction FileList({\n  files,\n  remove,\n  reduce,\n  itemMotion,\n}: {\n  files: File[]\n  remove: (key: string) => void\n  reduce: boolean | null\n  itemMotion?: {\n    initial: Record<string, number>\n    animate: Record<string, number>\n    exit: Record<string, number>\n    transition?: Record<string, unknown>\n  }\n}) {\n  return (\n    <ul data-slot=\"styled-file-input-list\" className=\"mt-3 flex flex-col gap-2\">\n      <AnimatePresence initial={false}>\n        {files.map((file) => (\n          <motion.li\n            key={fileKey(file)}\n            layout={!reduce}\n            initial={reduce || !itemMotion ? undefined : itemMotion.initial}\n            animate={reduce || !itemMotion ? undefined : itemMotion.animate}\n            exit={reduce || !itemMotion ? undefined : itemMotion.exit}\n            transition={itemMotion?.transition}\n            className=\"flex items-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm\"\n          >\n            <FileIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n            <span className=\"min-w-0 flex-1 truncate text-foreground\">{file.name}</span>\n            <span className=\"shrink-0 text-xs text-muted-foreground\">{formatSize(file.size)}</span>\n            <button\n              type=\"button\"\n              aria-label={`Remove ${file.name}`}\n              onClick={() => remove(fileKey(file))}\n              className={clearBtn}\n            >\n              <X className=\"size-4\" />\n            </button>\n          </motion.li>\n        ))}\n      </AnimatePresence>\n    </ul>\n  )\n}\n\n/* 1) DragFile: the surface grows while dragging and the icon lifts. Under reduced motion only the token colour state changes. */\nexport function DragFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose files to upload\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <motion.button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        data-drag-active={dragActive || undefined}\n        onClick={() => inputRef.current?.click()}\n        onDragOver={(e) => {\n          e.preventDefault()\n          setDragActive(true)\n        }}\n        onDragLeave={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n        }}\n        onDrop={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n          add(e.dataTransfer.files)\n        }}\n        initial={false}\n        animate={reduce ? undefined : { scale: dragActive ? 1.02 : 1 }}\n        transition={springy}\n        className={cn(surfaceBase, dropSurface[size])}\n      >\n        <motion.span\n          initial={false}\n          animate={reduce ? undefined : { y: dragActive ? -4 : 0 }}\n          transition={springy}\n          className={cn(\"flex items-center justify-center text-primary\", iconSize[size])}\n        >\n          <Upload className=\"size-full\" />\n        </motion.span>\n        <span className=\"font-medium text-foreground\">\n          {dragActive ? \"Release to drop\" : \"Drag files onto this zone\"}\n        </span>\n        <span className=\"text-xs text-muted-foreground\">The zone reacts while you drag</span>\n      </motion.button>\n\n      <FileList\n        files={files}\n        remove={remove}\n        reduce={reduce}\n        itemMotion={{\n          initial: { opacity: 0, y: -6 },\n          animate: { opacity: 1, y: 0 },\n          exit: { opacity: 0, y: -6 },\n        }}\n      />\n    </div>\n  )\n}\n\n/* 2) PopFile: the selected files enter with a springy pop. */\nexport function PopFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose files to upload\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        data-drag-active={dragActive || undefined}\n        onClick={() => inputRef.current?.click()}\n        onDragOver={(e) => {\n          e.preventDefault()\n          setDragActive(true)\n        }}\n        onDragLeave={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n        }}\n        onDrop={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n          add(e.dataTransfer.files)\n        }}\n        className={cn(surfaceBase, dropSurface[size])}\n      >\n        <Upload className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop files to pop them in</span>\n      </button>\n\n      <FileList\n        files={files}\n        remove={remove}\n        reduce={reduce}\n        itemMotion={{\n          initial: { opacity: 0, scale: 0.9 },\n          animate: { opacity: 1, scale: 1 },\n          exit: { opacity: 0, scale: 0.9 },\n          transition: springy,\n        }}\n      />\n    </div>\n  )\n}\n\n/* 3) FadeFile: the files enter and leave with a plain opacity transition. */\nexport function FadeFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose files to upload\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <motion.button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        data-drag-active={dragActive || undefined}\n        onClick={() => inputRef.current?.click()}\n        onDragOver={(e) => {\n          e.preventDefault()\n          setDragActive(true)\n        }}\n        onDragLeave={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n        }}\n        onDrop={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n          add(e.dataTransfer.files)\n        }}\n        initial={false}\n        animate={reduce ? undefined : { opacity: dragActive ? 1 : 0.85 }}\n        transition={{ duration: 0.2, ease: \"easeOut\" }}\n        className={cn(surfaceBase, dropSurface[size])}\n      >\n        <Upload className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop files to fade them in</span>\n      </motion.button>\n\n      <FileList\n        files={files}\n        remove={remove}\n        reduce={reduce}\n        itemMotion={{\n          initial: { opacity: 0 },\n          animate: { opacity: 1 },\n          exit: { opacity: 0 },\n          transition: { duration: 0.24, ease: \"easeOut\" },\n        }}\n      />\n    </div>\n  )\n}\n\n/* 4) SlideFile: dosyalar yandan kayarak girer, karsi yone kayarak cikar. */\nexport function SlideFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose files to upload\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        data-drag-active={dragActive || undefined}\n        onClick={() => inputRef.current?.click()}\n        onDragOver={(e) => {\n          e.preventDefault()\n          setDragActive(true)\n        }}\n        onDragLeave={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n        }}\n        onDrop={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n          add(e.dataTransfer.files)\n        }}\n        className={cn(surfaceBase, dropSurface[size])}\n      >\n        <Upload className={cn(iconSize[size], \"text-primary\")} />\n        <span className=\"font-medium text-foreground\">Drop files to slide them in</span>\n      </button>\n\n      <FileList\n        files={files}\n        remove={remove}\n        reduce={reduce}\n        itemMotion={{\n          initial: { opacity: 0, x: -16 },\n          animate: { opacity: 1, x: 0 },\n          exit: { opacity: 0, x: 16 },\n          transition: springy,\n        }}\n      />\n    </div>\n  )\n}\n\n/* 5) PulseFile: the surface breathes very lightly while empty and stops once a file is selected. The pulse is completely off under reduced motion. */\nexport function PulseFile({ className, size = \"md\", multiple = true, accept, onFilesChange }: FileInputProps) {\n  const reduce = useReducedMotion()\n  const id = React.useId()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const { files, add, remove } = useFiles(multiple, onFilesChange)\n  const idle = files.length === 0 && !dragActive && !reduce\n\n  return (\n    <div data-slot=\"styled-file-input\" className={cn(\"w-full\", className)}>\n      <label htmlFor={id} className=\"sr-only\">\n        Choose files to upload\n      </label>\n      <input\n        id={id}\n        ref={inputRef}\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className=\"sr-only\"\n        onChange={(e) => add(e.currentTarget.files)}\n      />\n      <button\n        type=\"button\"\n        data-slot=\"styled-file-input-surface\"\n        data-drag-active={dragActive || undefined}\n        onClick={() => inputRef.current?.click()}\n        onDragOver={(e) => {\n          e.preventDefault()\n          setDragActive(true)\n        }}\n        onDragLeave={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n        }}\n        onDrop={(e) => {\n          e.preventDefault()\n          setDragActive(false)\n          add(e.dataTransfer.files)\n        }}\n        className={cn(surfaceBase, dropSurface[size])}\n      >\n        <motion.span\n          initial={false}\n          animate={idle ? { scale: [1, 1.08, 1], opacity: [1, 0.7, 1] } : { scale: 1, opacity: 1 }}\n          transition={idle ? { duration: 1.8, repeat: Infinity, ease: \"easeInOut\" } : { duration: 0.2 }}\n          className={cn(\"flex items-center justify-center text-primary\", iconSize[size])}\n        >\n          <Upload className=\"size-full\" />\n        </motion.span>\n        <span className=\"font-medium text-foreground\">Waiting for files</span>\n        <span className=\"text-xs text-muted-foreground\">The icon settles once you pick one</span>\n      </button>\n\n      <FileList\n        files={files}\n        remove={remove}\n        reduce={reduce}\n        itemMotion={{\n          initial: { opacity: 0, scale: 0.96 },\n          animate: { opacity: 1, scale: 1 },\n          exit: { opacity: 0, scale: 0.96 },\n          transition: springy,\n        }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/file-input-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "file"
  ],
  "type": "registry:component"
}