diff --git a/src/app/globals.css b/src/app/globals.css
index 6ab1d77..949cf0a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -127,4 +127,45 @@
html {
@apply font-sans;
}
+}
+
+@keyframes paw-spring {
+ 0% {
+ transform: scale(1);
+ }
+ 35% {
+ transform: scale(1.3);
+ }
+ 60% {
+ transform: scale(0.92);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes paw-burst {
+ 0% {
+ transform: scale(0.6);
+ opacity: 0.9;
+ }
+ 100% {
+ transform: scale(2.1);
+ opacity: 0;
+ }
+}
+
+.animate-paw-spring {
+ animation: paw-spring 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+.animate-paw-burst {
+ animation: paw-burst 0.5s ease-out forwards;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .animate-paw-spring,
+ .animate-paw-burst {
+ animation: none;
+ }
}
\ No newline at end of file
diff --git a/src/components/feed/PawButton.tsx b/src/components/feed/PawButton.tsx
index abddf30..b9d9b7f 100644
--- a/src/components/feed/PawButton.tsx
+++ b/src/components/feed/PawButton.tsx
@@ -17,17 +17,24 @@ export function PawButton({ postId, petId, initialCount, initialReacted }: PawBu
const queryClient = useQueryClient();
const [reacted, setReacted] = useState(initialReacted);
const [count, setCount] = useState(initialCount);
+ const [animationKey, setAnimationKey] = useState(0);
+ const [showBurst, setShowBurst] = useState(false);
const toggleMutation = useMutation(
trpc.reactions.toggle.mutationOptions({
onMutate: () => {
const prevReacted = reacted;
const prevCount = count;
+ const nextReacted = !reacted;
// Optimistically flip the icon only — `reacted` can be stale (callers
// currently always pass initialReacted={false}), so count is corrected
// precisely from the server's actual `data.reacted` in onSuccess
// instead of guessing a +/-1 delta off a possibly-wrong prior state.
- setReacted(!reacted);
+ setReacted(nextReacted);
+ if (nextReacted) {
+ setAnimationKey((key) => key + 1);
+ setShowBurst(true);
+ }
return { prevReacted, prevCount };
},
onSuccess: (data, _vars, context) => {
@@ -61,14 +68,24 @@ export function PawButton({ postId, petId, initialCount, initialReacted }: PawBu
aria-label={reacted ? "Remove paw" : "Paw this post"}
className="flex items-center gap-1.5 text-sm transition-colors disabled:opacity-60"
>
-
+
+
+ {showBurst && (
+ setShowBurst(false)}
+ />
+ )}
+
{count > 0 && (
{count}