import type { GlobalOptions as ConfettiGlobalOptions, CreateTypes as ConfettiInstance, Options as ConfettiOptions, } from 'canvas-confetti'; import confetti from 'canvas-confetti'; import type { ReactNode } from 'react'; import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, } from 'react'; import { Button, ButtonProps } from './button'; type Api = { fire: (options?: ConfettiOptions) => void; }; type Props = React.ComponentPropsWithRef<'canvas'> & { options?: ConfettiOptions; globalOptions?: ConfettiGlobalOptions; manualstart?: boolean; children?: ReactNode; }; export type ConfettiRef = Api | null; const ConfettiContext = createContext({} as Api); const Confetti = forwardRef((props, ref) => { const { options, globalOptions = { resize: true, useWorker: true }, manualstart = false, children, ...rest } = props; const instanceRef = useRef(null); // confetti instance const canvasRef = useCallback( // https://react.dev/reference/react-dom/components/common#ref-callback // https://reactjs.org/docs/refs-and-the-dom.html#callback-refs (node: HTMLCanvasElement) => { if (node !== null) { // is mounted => create the confetti instance if (instanceRef.current) return; // if not already created instanceRef.current = confetti.create(node, { ...globalOptions, resize: true, }); } else { // is unmounted => reset and destroy instanceRef if (instanceRef.current) { instanceRef.current.reset(); instanceRef.current = null; } } }, [globalOptions], ); // `fire` is a function that calls the instance() with `opts` merged with `options` const fire = useCallback( (opts = {}) => instanceRef.current?.({ ...options, ...opts }), [options], ); const api = useMemo( () => ({ fire, }), [fire], ); useImperativeHandle(ref, () => api, [api]); useEffect(() => { if (!manualstart) { fire(); } }, [manualstart, fire]); return ( {children} ); }); interface ConfettiButtonProps extends ButtonProps { options?: ConfettiOptions & ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }; children?: React.ReactNode; } function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) { const handleClick = (event: React.MouseEvent) => { const rect = event.currentTarget.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; confetti({ ...options, origin: { x: x / window.innerWidth, y: y / window.innerHeight, }, }); }; return ( ); } Confetti.displayName = 'Confetti'; export { Confetti, ConfettiButton }; export default Confetti;