{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A InteractiveImageSelector component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Share2, Trash2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useState } from \"react\";\n\nconst RESET_DELAY = 200;\nconst RESET_SCALE_START = 1;\nconst RESET_SCALE_PEAK = 1.1;\nconst RESET_SCALE_END = 1;\nconst RESET_ROTATE_START = 0;\nconst RESET_ROTATE_POSITIVE = 5;\nconst RESET_ROTATE_NEGATIVE = -5;\nconst SELECT_SCALE_START = 1;\nconst SELECT_SCALE_PEAK = 1.1;\nconst SELECT_SCALE_END = 1;\nconst SELECT_ROTATE_START = 0;\nconst SELECT_ROTATE_NEGATIVE = -5;\nconst SELECT_ROTATE_POSITIVE = 5;\nconst CONTAINER_SCALE_START = 1;\nconst CONTAINER_SCALE_MIN = 0.95;\nconst CONTAINER_SCALE_END = 1;\nconst ITEM_SCALE_START = 1;\nconst ITEM_SCALE_MIN = 0.9;\nconst ITEM_SCALE_END = 1;\nconst ITEM_ROTATE_START = 0;\nconst ITEM_ROTATE_POSITIVE = 2;\nconst ITEM_ROTATE_NEGATIVE = -2;\nconst RESET_ANIMATION_DURATION = 0.3;\n\nexport interface ImageData {\n  id: number;\n  src: string;\n}\n\nexport interface InteractiveImageSelectorProps {\n  className?: string;\n  images: ImageData[];\n  onChange?: (selected: number[]) => void;\n  onDelete?: (deleted: number[]) => void;\n  onShare?: (selected: number[]) => void;\n  selectable?: boolean;\n  selectedImages?: number[];\n}\n\nexport default function InteractiveImageSelector({\n  images,\n  selectedImages: controlledSelected,\n  onChange,\n  onDelete,\n  onShare,\n  className = \"\",\n  selectable = false,\n}: InteractiveImageSelectorProps) {\n  const [originalImages] = useState<ImageData[]>(images);\n  const [internalImages, setInternalImages] = useState<ImageData[]>(images);\n  const [internalSelected, setInternalSelected] = useState<number[]>([]);\n  const [isSelecting, setIsSelecting] = useState(selectable);\n  const [isResetting, setIsResetting] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const selected = controlledSelected ?? internalSelected;\n\n  const handleImageClick = useCallback(\n    (id: number) => {\n      if (!isSelecting) {\n        return;\n      }\n      const newSelected = selected.includes(id)\n        ? selected.filter((imgId) => imgId !== id)\n        : [...selected, id];\n      if (onChange) {\n        onChange(newSelected);\n      } else {\n        setInternalSelected(newSelected);\n      }\n    },\n    [isSelecting, selected, onChange]\n  );\n\n  const handleDelete = useCallback(() => {\n    const newImages = internalImages.filter(\n      (img) => !selected.includes(img.id)\n    );\n    if (onDelete) {\n      onDelete(selected);\n    }\n    setInternalImages(newImages);\n    if (onChange) {\n      onChange([]);\n    } else {\n      setInternalSelected([]);\n    }\n  }, [selected, internalImages, onDelete, onChange]);\n\n  const handleReset = useCallback(() => {\n    setIsResetting(true);\n\n    // Add a small delay to show the reset animation\n    setTimeout(() => {\n      setInternalImages(originalImages);\n      if (onChange) {\n        onChange([]);\n      } else {\n        setInternalSelected([]);\n      }\n      setIsSelecting(false);\n      setIsResetting(false);\n    }, RESET_DELAY);\n  }, [originalImages, onChange]);\n\n  const toggleSelecting = useCallback(() => {\n    setIsSelecting((prev) => !prev);\n    if (isSelecting) {\n      if (onChange) {\n        onChange([]);\n      } else {\n        setInternalSelected([]);\n      }\n    }\n  }, [isSelecting, onChange]);\n\n  const handleShare = useCallback(() => {\n    if (onShare) {\n      onShare(selected);\n    }\n  }, [onShare, selected]);\n\n  return (\n    <div\n      className={`relative flex h-full w-full max-w-[500px] flex-col justify-between p-4 ${className}`}\n    >\n      <div className=\"pointer-events-none absolute inset-x-0 top-0 z-10 h-28 bg-linear-to-b from-primary/80 to-transparent dark:from-background/50\" />\n      <div className=\"absolute top-5 right-5 left-5 z-20 flex justify-between p-4\">\n        <motion.button\n          animate={\n            shouldReduceMotion || !isResetting\n              ? {}\n              : {\n                  rotate: [\n                    RESET_ROTATE_START,\n                    RESET_ROTATE_POSITIVE,\n                    RESET_ROTATE_NEGATIVE,\n                    RESET_ROTATE_START,\n                  ],\n                  scale: [RESET_SCALE_START, RESET_SCALE_PEAK, RESET_SCALE_END],\n                }\n          }\n          aria-label=\"Reset selection\"\n          className={`cursor-pointer rounded-full px-3 py-1 font-semibold text-sm bg-blend-luminosity backdrop-blur-xl transition-colors ${\n            isResetting\n              ? \"bg-brand/30 text-white\"\n              : \"bg-background/20 text-foreground\"\n          }`}\n          disabled={isResetting}\n          exit={shouldReduceMotion ? {} : { rotate: 0 }}\n          initial={shouldReduceMotion ? {} : { rotate: 0 }}\n          onClick={handleReset}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }}\n          whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n          whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n        >\n          {isResetting ? \"Resetting...\" : \"Reset\"}\n        </motion.button>\n        <motion.button\n          animate={\n            isSelecting\n              ? {\n                  rotate: [\n                    SELECT_ROTATE_START,\n                    SELECT_ROTATE_NEGATIVE,\n                    SELECT_ROTATE_POSITIVE,\n                    SELECT_ROTATE_START,\n                  ],\n                  scale: [\n                    SELECT_SCALE_START,\n                    SELECT_SCALE_PEAK,\n                    SELECT_SCALE_END,\n                  ],\n                }\n              : {}\n          }\n          aria-label={isSelecting ? \"Cancel selection\" : \"Select images\"}\n          className={`cursor-pointer rounded-full px-3 py-1 font-semibold text-sm bg-blend-luminosity backdrop-blur-xl ${\n            isSelecting\n              ? \"bg-brand/30 text-white\"\n              : \"bg-background/20 text-foreground\"\n          }`}\n          exit={{ rotate: 0 }}\n          initial={{ rotate: 0 }}\n          onClick={toggleSelecting}\n          transition={{ duration: 0.3 }}\n          type=\"button\"\n          whileHover={{ scale: 1.05 }}\n          whileTap={{ scale: 0.95 }}\n        >\n          {isSelecting ? \"Cancel\" : \"Select\"}\n        </motion.button>\n      </div>\n\n      <motion.div\n        animate={\n          shouldReduceMotion || !isResetting\n            ? {}\n            : {\n                scale: [\n                  CONTAINER_SCALE_START,\n                  CONTAINER_SCALE_MIN,\n                  CONTAINER_SCALE_END,\n                ],\n              }\n        }\n        className=\"grid grid-cols-3 gap-1 overflow-scroll\"\n        layout={!shouldReduceMotion}\n        transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n      >\n        <AnimatePresence>\n          {internalImages.map((img) => (\n            <motion.div\n              animate={{\n                opacity: 1,\n                rotate: isResetting\n                  ? [\n                      ITEM_ROTATE_START,\n                      ITEM_ROTATE_POSITIVE,\n                      ITEM_ROTATE_NEGATIVE,\n                      ITEM_ROTATE_START,\n                    ]\n                  : 0,\n                scale: isResetting\n                  ? [ITEM_SCALE_START, ITEM_SCALE_MIN, ITEM_SCALE_END]\n                  : 1,\n              }}\n              className=\"relative aspect-square cursor-pointer\"\n              exit={{ opacity: 0, scale: 0.8 }}\n              initial={{ opacity: 0, scale: 0.8 }}\n              key={img.id}\n              layout\n              onClick={() => handleImageClick(img.id)}\n              transition={{\n                damping: 25,\n                duration: isResetting ? RESET_ANIMATION_DURATION : undefined,\n                stiffness: 300,\n                type: \"spring\" as const,\n              }}\n            >\n              <img\n                alt={`Gallery item ${img.id}`}\n                className={`h-full w-full rounded-lg object-cover ${\n                  selected.includes(img.id) && isSelecting ? \"opacity-75\" : \"\"\n                }`}\n                draggable={false}\n                height={200}\n                loading=\"lazy\"\n                src={img.src}\n                width={200}\n              />\n              {isSelecting && selected.includes(img.id) && (\n                <div className=\"absolute right-2 bottom-2 flex h-6 w-6 items-center justify-center rounded-full border border-white bg-brand text-white\">\n                  ✓\n                </div>\n              )}\n            </motion.div>\n          ))}\n        </AnimatePresence>\n      </motion.div>\n      <AnimatePresence>\n        {isSelecting ? (\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className=\"absolute right-2 bottom-0 left-1/2 z-10 flex w-2/3 -translate-x-1/2 items-center justify-between rounded-full bg-background/20 p-4 bg-blend-luminosity backdrop-blur-md\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: 20 }\n            }\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n            }\n            transition={\n              shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }\n            }\n          >\n            <button\n              className=\"cursor-pointer text-brand\"\n              onClick={handleShare}\n              type=\"button\"\n            >\n              <Share2 size={24} />\n            </button>\n            <span className=\"text-foreground\">{selected.length} selected</span>\n            <button\n              className=\"cursor-pointer text-brand\"\n              disabled={selected.length === 0}\n              onClick={handleDelete}\n              type=\"button\"\n            >\n              <Trash2 size={24} />\n            </button>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/interactive-image-selector/index.tsx","type":"registry:ui"}],"name":"interactive-image-selector","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Interactive Image Selector","type":"registry:ui"}