component Confetti { outputs { start(payload: { sourceEvent?: Event; duration: number }) complete(payload: { sourceEvent?: Event; duration: number }) } props { label: string = "Celebrate" duration: number = 1200 count: number = 80 autoStart: boolean = false disabled: boolean = false size: string = "default" color: string = "primary" class: string = "" } state running = false functions { shared function pieces() { return Array.from({ length: Math.max(1, Math.min(200, count)) }) } shared function pieceStyle(index) { return "--wrn-confetti-index:" + index + ";--wrn-confetti-x:" + ((index * 37) % 100) + ";--wrn-confetti-delay:" + 0 + "ms;" } client function animateFrame(canvas, particles, started) { var context = canvas.getContext("2d") var progress = (Date.now() - started) / Math.max(1, duration) context.clearRect(0, 0, 720, 420) if (progress >= 1 || !canvas.isConnected) { return } particles.forEach(function (piece) { piece.vx = piece.vx * 0.99 piece.vy = piece.vy + 0.19 piece.x = piece.x + piece.vx piece.y = piece.y + piece.vy piece.rotation = piece.rotation + piece.spin context.save() context.translate(piece.x, piece.y) context.rotate(piece.rotation) context.globalAlpha = Math.min(1, (1 - progress) * 4) context.fillStyle = piece.color context.fillRect(-3, -4, 6, 8 * Math.cos(piece.rotation)) context.restore() }) setTimeout(animateFrame, 16, canvas, particles, started) } client function animateBurst(sourceEvent) { var root = sourceEvent.currentTarget.closest(".wrn-confetti") var canvas = root.querySelector("canvas") if (!canvas || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { return } var context = canvas.getContext("2d") if (!context) { return } canvas.width = 720 canvas.height = 420 var particles = Array.from({length:Math.min(200, Math.max(1, count))}, function (_, index) { var angle = (-155 + Math.random() * 130) * Math.PI / 180 var speed = 5 + Math.random() * 9 return {x:360,y:273,vx:Math.cos(angle) * speed,vy:Math.sin(angle) * speed,rotation:Math.random() * 6,spin:(Math.random() - 0.5) * 0.3,color:["#8b5cf6","#14b8a6","#f59e0b","#f472b6","#60a5fa"][index % 5]} }) animateFrame(canvas, particles, Date.now()) } client function finish(sourceEvent) { running = false output.complete({ sourceEvent: sourceEvent, duration: duration }) } client function start(sourceEvent) { if (disabled || running) { return } running = true if (sourceEvent) { animateBurst(sourceEvent) } output.start({ sourceEvent: sourceEvent, duration: duration }) setTimeout(finish, duration, sourceEvent) } } lifecycle { mount { if (autoStart) { start(null) } } } view {