File size: 2,691 Bytes
02e775b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
'use client';

import { createContext, useContext, useCallback, useState, ReactNode } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Check } from 'lucide-react';

/**
 * OptimisticUI — provides instant feedback for user actions.
 * 
 * - Cart fly animation: when "Add to Cart" is clicked, a checkmark
 *   bursts with a bouncy spring animation at the click location.
 * - Like burst: when a like button is clicked, a small confetti-like
 *   burst plays.
 * 
 * Usage:
 *   const { burst } = useOptimisticUI();
 *   <button onClick={(e) => burst(e.clientX, e.clientY)}>Add to Cart</button>
 */

interface Burst {
  id: number;
  x: number;
  y: number;
  type: 'check' | 'heart';
}

const OptimisticContext = createContext<{ burst: (x: number, y: number, type?: 'check' | 'heart') => void }>({
  burst: () => {},
});

export function OptimisticUIProvider({ children }: { children: ReactNode }) {
  const [bursts, setBursts] = useState<Burst[]>([]);

  const burst = useCallback((x: number, y: number, type: 'check' | 'heart' = 'check') => {
    const id = Date.now() + Math.random();
    setBursts(prev => [...prev, { id, x, y, type }]);
    setTimeout(() => {
      setBursts(prev => prev.filter(b => b.id !== id));
    }, 800);
  }, []);

  return (
    <OptimisticContext.Provider value={{ burst }}>
      {children}
      {/* Burst animations overlay */}
      <div className="fixed inset-0 z-[10000] pointer-events-none">
        <AnimatePresence>
          {bursts.map(b => (
            <motion.div
              key={b.id}
              initial={{ scale: 0, x: b.x - 20, y: b.y - 20, opacity: 1 }}
              animate={{ scale: [0, 1.3, 1], y: b.y - 60, opacity: [1, 1, 0] }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.8, ease: [0.32, 0.72, 0, 1] }}
              className="absolute w-10 h-10 flex items-center justify-center"
            >
              {b.type === 'check' ? (
                <div className="w-10 h-10 rounded-full bg-black flex items-center justify-center">
                  <Check className="w-5 h-5 text-white" strokeWidth={3} />
                </div>
              ) : (
                <svg width="28" height="28" viewBox="0 0 24 24" fill="white" stroke="black" strokeWidth="1">
                  <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
                </svg>
              )}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </OptimisticContext.Provider>
  );
}

export function useOptimisticUI() {
  return useContext(OptimisticContext);
}