{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ripple-dropzone",
  "title": "Ripple Dropzone",
  "description": "A dropzone whose rings ripple while dragging, with a burst on drop.",
  "dependencies": [
    "@uploadcn/core@^0.1.0",
    "@uploadcn/react@^0.1.0",
    "cn",
    "motion"
  ],
  "registryDependencies": [
    "@uploadcn/upload"
  ],
  "files": [
    {
      "path": "registry/default/blocks/ripple-dropzone.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 { useUploadContext, type UploadRootProps } from \"@uploadcn/react\"\nimport { cn } from \"cn\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\nimport {\n  Upload,\n  UploadDropzone,\n  UploadDropzoneDescription,\n  UploadDropzoneTitle,\n  UploadQueue,\n} from \"@/registry/default/ui/upload\"\n\ninterface RippleDropzoneProps<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  title?: React.ReactNode\n  description?: React.ReactNode\n  onSuccess?: (item: UploadItemData<TResult>) => void\n  className?: string\n}\n\n/**\n * A dropzone with a pulse: rings ripple out while files are dragged over it,\n * and a burst confirms the drop.\n */\nfunction RippleDropzone<TResult = unknown>({\n  adapter,\n  uploader,\n  title = \"Drop to upload\",\n  description = \"Drag files here, or click to browse\",\n  className,\n  ...options\n}: RippleDropzoneProps<TResult>) {\n  return (\n    <Upload\n      {...((uploader\n        ? { uploader }\n        : {\n            adapter,\n            ...options,\n          }) as UploadRootProps<TResult>)}\n      className={className}\n    >\n      <UploadDropzone\n        variant=\"muted\"\n        size=\"lg\"\n        render={(props, state) => (\n          <div {...props}>\n            <Ripples dragging={state.isDragging} reject={state.isDragReject} />\n            {props.children as React.ReactNode}\n          </div>\n        )}\n      >\n        <UploadDropzoneTitle className=\"mt-2\">{title}</UploadDropzoneTitle>\n        <UploadDropzoneDescription>{description}</UploadDropzoneDescription>\n      </UploadDropzone>\n      <UploadQueue />\n    </Upload>\n  )\n}\n\nfunction Ripples({ dragging, reject }: { dragging: boolean; reject: boolean }) {\n  const { uploader } = useUploadContext()\n  const reduceMotion = useReducedMotion()\n  const [bursts, setBursts] = React.useState<number[]>([])\n\n  React.useEffect(\n    () =>\n      uploader.on(\"add\", () => {\n        if (!reduceMotion) setBursts((current) => [...current, Date.now()])\n      }),\n    [uploader, reduceMotion]\n  )\n\n  return (\n    <div\n      aria-hidden\n      className=\"relative flex size-14 items-center justify-center\"\n    >\n      {dragging && !reduceMotion\n        ? [0, 1, 2].map((ring) => (\n            <motion.span\n              key={ring}\n              className={cn(\n                \"absolute inset-0 rounded-full border\",\n                reject ? \"border-destructive/60\" : \"border-primary/40\"\n              )}\n              initial={{ scale: 1, opacity: 0.6 }}\n              animate={{ scale: 2.6, opacity: 0 }}\n              transition={{\n                duration: 1.6,\n                repeat: Infinity,\n                delay: ring * 0.5,\n                ease: \"easeOut\",\n              }}\n            />\n          ))\n        : null}\n      <AnimatePresence>\n        {bursts.map((burst) => (\n          <motion.span\n            key={burst}\n            className=\"absolute inset-0 rounded-full bg-primary/25\"\n            initial={{ scale: 0.8, opacity: 0.8 }}\n            animate={{ scale: 3, opacity: 0 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.7, ease: \"easeOut\" }}\n            onAnimationComplete={() =>\n              setBursts((current) => current.filter((item) => item !== burst))\n            }\n          />\n        ))}\n      </AnimatePresence>\n      <motion.span\n        className={cn(\n          \"relative flex size-14 items-center justify-center rounded-full border bg-background shadow-sm transition-colors\",\n          dragging && !reject && \"border-primary text-foreground\",\n          reject && \"border-destructive text-destructive\"\n        )}\n        animate={reduceMotion ? undefined : { scale: dragging ? 1.12 : 1 }}\n        transition={{ type: \"spring\", bounce: 0.45, duration: 0.5 }}\n      >\n        <motion.span\n          className=\"flex\"\n          animate={reduceMotion || !dragging ? { y: 0 } : { y: [0, -4, 0] }}\n          transition={\n            dragging\n              ? { repeat: Infinity, duration: 0.9, ease: \"easeInOut\" }\n              : { duration: 0.2 }\n          }\n        >\n          <IconPlaceholder\n            lucide=\"ArrowUpIcon\"\n            tabler=\"IconArrowUp\"\n            hugeicons=\"ArrowUpIcon\"\n            phosphor=\"ArrowUpIcon\"\n            remixicon=\"RiArrowUpLine\"\n            className=\"size-5\"\n          />\n        </motion.span>\n      </motion.span>\n    </div>\n  )\n}\n\nexport { RippleDropzone, type RippleDropzoneProps }\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "upload",
    "animated"
  ],
  "type": "registry:block"
}