aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/components/ui/confetti.tsx
diff options
context:
space:
mode:
authorMax Nanis2026-02-18 20:42:03 -0500
committerMax Nanis2026-02-18 20:42:03 -0500
commit3eaa56f0306ead818f64c3d99fc6d230d9b970a4 (patch)
tree9fecc2f1456e6321572e0e65f57106916df173e2 /jb-ui/src/components/ui/confetti.tsx
downloadamt-jb-3eaa56f0306ead818f64c3d99fc6d230d9b970a4.tar.gz
amt-jb-3eaa56f0306ead818f64c3d99fc6d230d9b970a4.zip
HERE WE GO, HERE WE GO, HERE WE GO
Diffstat (limited to 'jb-ui/src/components/ui/confetti.tsx')
-rw-r--r--jb-ui/src/components/ui/confetti.tsx146
1 files changed, 146 insertions, 0 deletions
diff --git a/jb-ui/src/components/ui/confetti.tsx b/jb-ui/src/components/ui/confetti.tsx
new file mode 100644
index 0000000..e0d353a
--- /dev/null
+++ b/jb-ui/src/components/ui/confetti.tsx
@@ -0,0 +1,146 @@
+import type { ReactNode } from "react"
+import React, {
+ createContext,
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+} from "react"
+import type {
+ GlobalOptions as ConfettiGlobalOptions,
+ CreateTypes as ConfettiInstance,
+ Options as ConfettiOptions,
+} from "canvas-confetti"
+import confetti from "canvas-confetti"
+
+import { Button } from "@/components/ui/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<Api>({} as Api)
+
+// Define component first
+const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => {
+ const {
+ options,
+ globalOptions = { resize: true, useWorker: true },
+ manualstart = false,
+ children,
+ ...rest
+ } = props
+ const instanceRef = useRef<ConfettiInstance | null>(null)
+
+ const canvasRef = useCallback(
+ (node: HTMLCanvasElement) => {
+ if (node !== null) {
+ if (instanceRef.current) return
+ instanceRef.current = confetti.create(node, {
+ ...globalOptions,
+ resize: true,
+ })
+ } else {
+ if (instanceRef.current) {
+ instanceRef.current.reset()
+ instanceRef.current = null
+ }
+ }
+ },
+ [globalOptions]
+ )
+
+ const fire = useCallback(
+ async (opts = {}) => {
+ try {
+ await instanceRef.current?.({ ...options, ...opts })
+ } catch (error) {
+ console.error("Confetti error:", error)
+ }
+ },
+ [options]
+ )
+
+ const api = useMemo(
+ () => ({
+ fire,
+ }),
+ [fire]
+ )
+
+ useImperativeHandle(ref, () => api, [api])
+
+ useEffect(() => {
+ if (!manualstart) {
+ ;(async () => {
+ try {
+ await fire()
+ } catch (error) {
+ console.error("Confetti effect error:", error)
+ }
+ })()
+ }
+ }, [manualstart, fire])
+
+ return (
+ <ConfettiContext.Provider value={api}>
+ <canvas ref={canvasRef} {...rest} />
+ {children}
+ </ConfettiContext.Provider>
+ )
+})
+
+// Set display name immediately
+ConfettiComponent.displayName = "Confetti"
+
+// Export as Confetti
+export const Confetti = ConfettiComponent
+
+interface ConfettiButtonProps extends React.ComponentProps<"button"> {
+ options?: ConfettiOptions &
+ ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }
+}
+
+const ConfettiButtonComponent = ({
+ options,
+ children,
+ ...props
+}: ConfettiButtonProps) => {
+ const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
+ try {
+ const rect = event.currentTarget.getBoundingClientRect()
+ const x = rect.left + rect.width / 2
+ const y = rect.top + rect.height / 2
+ await confetti({
+ ...options,
+ origin: {
+ x: x / window.innerWidth,
+ y: y / window.innerHeight,
+ },
+ })
+ } catch (error) {
+ console.error("Confetti button error:", error)
+ }
+ }
+
+ return (
+ <Button onClick={handleClick} {...props}>
+ {children}
+ </Button>
+ )
+}
+
+ConfettiButtonComponent.displayName = "ConfettiButton"
+
+export const ConfettiButton = ConfettiButtonComponent