React + MotionMicrointeracciones

Reacciones flotantes

Una barra de reacciones tipo Dock: los emojis crecen al acercarte y cada toque suelta unos cuantos que suben flotando.

Ajustes

3

Código

import { useRef, useState } from "react";
import { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
import type { MotionValue } from "framer-motion";

interface Floater {
  id: number;
  emoji: string;
  x: number;
  drift: number;
  rot: number;
  size: number;
  rise: number;
  delay: number;
}

interface DockEmojiProps {
  emoji: string;
  mouseX: MotionValue<number>;
  onReact: (emoji: string, clientX: number) => void;
}

function DockEmoji({ emoji, mouseX, onReact }: DockEmojiProps) {
  const ref = useRef<HTMLButtonElement>(null);
  const distance = useTransform(mouseX, (x) => {
    const r = ref.current?.getBoundingClientRect();
    return r ? x - (r.left + r.width / 2) : Infinity;
  });
  const size = useSpring(useTransform(distance, [-96, 0, 96], [40, 56, 40]), {
    stiffness: 420,
    damping: 30,
    mass: 0.4,
  });
  const fontSize = useTransform(size, (s) => s * 0.56);

  return (
    <motion.button
      ref={ref}
      type="button"
      aria-label={"Reaccionar con " + emoji}
      onClick={(e) => onReact(emoji, e.currentTarget.getBoundingClientRect().left + e.currentTarget.offsetWidth / 2)}
      whileTap={{ scale: 0.8 }}
      transition={{ type: "spring", stiffness: 600, damping: 20 }}
      style={{ width: size, height: size, fontSize }}
      className="grid shrink-0 place-items-center rounded-full leading-none outline-none focus-visible:ring-2 focus-visible:ring-black/20 dark:focus-visible:ring-white/30"
    >
      {emoji}
    </motion.button>
  );
}

interface LiveReactionsProps {
  emojis?: string;
  burst?: number;
}

export default function LiveReactions({
  emojis = "👍 ❤️ 😂 😮 🔥",
  burst = 3,
}: LiveReactionsProps) {
  const list = emojis.split(/\s+/).filter(Boolean).slice(0, 6);
  const [floaters, setFloaters] = useState<Floater[]>([]);
  const stage = useRef<HTMLDivElement>(null);
  const nextId = useRef(0);
  const mouseX = useMotionValue(Infinity);
  const reduce = useReducedMotion();

  const react = (emoji: string, clientX: number) => {
    const left = stage.current?.getBoundingClientRect().left ?? 0;
    const items = Array.from({ length: burst }, (_, i) => ({
      id: nextId.current++,
      emoji,
      x: clientX - left + (Math.random() - 0.5) * 14,
      drift: (Math.random() - 0.5) * 56,
      rot: (Math.random() - 0.5) * 40,
      size: 22 + Math.random() * 12,
      rise: 120 + Math.random() * 60,
      delay: i * 0.08,
    }));
    setFloaters((f) => [...f, ...items].slice(-36));
  };

  return (
    <div ref={stage} className="relative flex h-60 w-full max-w-xs items-end justify-center">
      <div className="pointer-events-none absolute inset-0" aria-hidden="true">
        <AnimatePresence>
          {floaters.map((p) => (
            <motion.span
              key={p.id}
              className="absolute bottom-14 select-none leading-none"
              style={{ left: p.x, fontSize: p.size, marginLeft: -p.size / 2 }}
              initial={{ y: 0, opacity: 0, scale: 0.4 }}
              animate={{
                y: reduce ? -40 : -p.rise,
                x: reduce ? 0 : [0, p.drift * 0.5, -p.drift * 0.3, p.drift],
                rotate: reduce ? 0 : [0, p.rot, -p.rot * 0.5, p.rot * 0.3],
                opacity: [0, 1, 1, 0],
                scale: [0.4, 1.1, 1, 0.9],
              }}
              transition={{ duration: 1.8, delay: p.delay, ease: [0.22, 1, 0.36, 1] }}
              onAnimationComplete={() => setFloaters((f) => f.filter((q) => q.id !== p.id))}
            >
              {p.emoji}
            </motion.span>
          ))}
        </AnimatePresence>
      </div>
      <div
        onPointerMove={(e) => mouseX.set(e.clientX)}
        onPointerDown={(e) => mouseX.set(e.clientX)}
        onPointerLeave={() => mouseX.set(Infinity)}
        className="relative flex items-end gap-1 rounded-full bg-white/85 p-1.5 shadow-[0_0_0_1px_rgba(0,0,0,0.05),0_1px_2px_rgba(0,0,0,0.05),0_12px_32px_-8px_rgba(0,0,0,0.16)] backdrop-blur-xl dark:bg-[#2c2c2e]/85 dark:shadow-[0_0_0_1px_rgba(255,255,255,0.08),0_12px_32px_-8px_rgba(0,0,0,0.6)]"
      >
        {list.map((emoji, i) => (
          <DockEmoji key={emoji + i} emoji={emoji} mouseX={mouseX} onReact={react} />
        ))}
      </div>
    </div>
  );
}

Prompt para IA

Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.

Más en Microinteracciones

Dónde usarlo

Las reacciones con emojis flotantes le dan ruido a un momento en directo: un streaming, un webinar, un evento online o el final de un post. La barra se amplía como el Dock de macOS bajo el cursor, y cada toque suelta de uno a seis emojis que suben girando, como un público de verdad.

La lupa necesita ratón; en el móvil la gente toca y los emojis suben igual. Quédate en cinco o seis reacciones. Los emojis se ven algo distintos en cada sistema, así que revisa el conjunto en un iPhone y en un Android.