animated-tags

PreviousNext

A AnimatedTags component for SmoothUI.

Docs
smoothuiui

Preview

Loading preview…
index.tsx
"use client";

import { CircleX, Plus } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";

export type AnimatedTagsProps = {
  initialTags?: string[];
  selectedTags?: string[];
  onChange?: (selected: string[]) => void;
  className?: string;
};

export default function AnimatedTags({
  initialTags = ["react", "tailwindcss", "javascript"],
  selectedTags: controlledSelectedTags,
  onChange,
  className = "",
}: AnimatedTagsProps) {
  const [internalSelected, setInternalSelected] = useState<string[]>([]);

  const selectedTag = controlledSelectedTags ?? internalSelected;
  const tags = initialTags.filter((tag) => !selectedTag.includes(tag));

  const handleTagClick = (tag: string) => {
    const newSelected = [...selectedTag, tag];
    if (onChange) {
      onChange(newSelected);
    } else {
      setInternalSelected(newSelected);
    }
  };
  const handleDeleteTag = (tag: string) => {
    const newSelectedTag = selectedTag.filter((selected) => selected !== tag);
    if (onChange) {
      onChange(newSelectedTag);
    } else {
      setInternalSelected(newSelectedTag);
    }
  };
  return (
    <div className={`flex w-[300px] flex-col gap-4 p-4 ${className}`}>
      <div className="flex flex-col items-start justify-center gap-1">
        <p>Selected Tags</p>
        <AnimatePresence>
          <div className="flex min-h-12 w-full flex-wrap items-center gap-1 rounded-xl border bg-background p-2">
            {selectedTag?.map((tag) => (
              <motion.div
                animate={{
                  y: 0,
                  opacity: 1,
                  filter: "blur(0px)",
                }}
                className="group flex cursor-pointer flex-row items-center justify-center gap-2 rounded-md border bg-primary px-2 py-1 text-primary-foreground group-hover:bg-primary group-hover:text-foreground"
                exit={{ y: 20, opacity: 0, filter: "blur(4px)" }}
                initial={{ y: 20, opacity: 0, filter: "blur(4px)" }}
                key={tag}
                layout
                onClick={() => handleDeleteTag(tag)}
                transition={{ duration: 0.3, bounce: 0, type: "spring" }}
              >
                {tag}{" "}
                <CircleX
                  className="flex items-center justify-center rounded-full transition-all duration-300 ease-in-out"
                  size={16}
                />
              </motion.div>
            ))}
          </div>
        </AnimatePresence>
      </div>
      <AnimatePresence>
        <div className="flex flex-wrap items-center gap-1">
          {tags.map((tag) => (
            <motion.div
              animate={{
                y: 0,
                opacity: 1,
                filter: "blur(0px)",
              }}
              className="group flex cursor-pointer flex-row items-center justify-center gap-2 rounded-md border bg-background px-2 py-1 text-primary-foreground"
              exit={{ y: -20, opacity: 0, filter: "blur(4px)" }}
              initial={{ y: -20, opacity: 0, filter: "blur(4px)" }}
              key={tag}
              layout
              onClick={() => handleTagClick(tag)}
              transition={{ duration: 0.3, bounce: 0, type: "spring" }}
            >
              {tag}{" "}
              <Plus
                className="flex items-center justify-center rounded-full transition-all duration-300 ease-in-out hover:bg-primary group-hover:text-foreground"
                size={16}
              />
            </motion.div>
          ))}
        </div>
      </AnimatePresence>
    </div>
  );
}

Installation

npx shadcn@latest add @smoothui/animated-tags

Usage

import { AnimatedTags } from "@/components/ui/animated-tags"
<AnimatedTags />