{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "onomatopoeia", "registryDependencies": [ "https://powui.dev/r/utils.json", "https://powui.dev/r/theme.json" ], "files": [ { "path": "src/components/ui/onomatopoeia.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useCallback, useMemo, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\ntype TEvaluateContainerStyleArgs = {\n x: number;\n y: number;\n duration?: number;\n addRandomness?: boolean;\n displayElement?: React.ReactNode;\n};\n\nconst ClickBurst = () => {\n const lineLength = 20;\n\n const allPoints = Array.from({ length: 8 }).map((_, i) => {\n const angle = (i * 45 * Math.PI) / 180;\n return {\n x1: lineLength,\n y1: lineLength,\n x2: lineLength + Math.cos(angle) * lineLength,\n y2: lineLength + Math.sin(angle) * lineLength,\n };\n });\n\n const svg = (\n \n {allPoints.map((point, index) => (\n \n ))}\n \n );\n\n return svg;\n};\n\nconst evaluateContainerStyle = ({\n x,\n y,\n duration,\n addRandomness = true,\n}: TEvaluateContainerStyleArgs) => {\n const evaluatedContainerStyle: CSSProperties = {\n position: \"absolute\" as const,\n left: x,\n top: y,\n transition: `transform ${duration}ms`,\n };\n if (addRandomness) {\n const radius = 20;\n const minimumDistance = 10;\n const translateX =\n (Math.random() > 0.5 ? 1 : -1) *\n (minimumDistance + Math.random() * radius);\n const translateY =\n (Math.random() > 0.5 ? 1 : -1) *\n (minimumDistance + Math.random() * radius);\n\n const angle = Math.atan(translateY / translateX);\n const rotationAngleNotAllowed = (180 * angle) / Math.PI;\n\n let rotationAngle = (Math.random() - 0.5) * 100;\n\n if (Math.abs(rotationAngle - rotationAngleNotAllowed) < 20) {\n rotationAngle = rotationAngle + 20;\n }\n\n evaluatedContainerStyle.translate = `calc(-50% + ${translateX}px) calc(-50% + ${translateY}px)`;\n evaluatedContainerStyle.rotate = `${rotationAngle}deg`;\n }\n return evaluatedContainerStyle;\n};\n\nexport const useEventOnomatopoeia = (props: {\n containerStyle?: CSSProperties;\n showClickBurst?: boolean;\n displayElement?: React.ReactNode;\n}) => {\n const { containerStyle = {}, showClickBurst = false, displayElement } = props;\n const [portalElements, setPortalElements] = useState>({});\n const trigger = useCallback(\n (args: TEvaluateContainerStyleArgs) => {\n const {\n x,\n y,\n duration = 200,\n displayElement: triggerDisplayElement,\n } = args;\n if (!x || !y) return;\n const evaluatedContainerStyle = evaluateContainerStyle(args);\n const id = window.crypto.randomUUID();\n\n const finalDisplayElement = triggerDisplayElement ?? displayElement;\n\n const items = [\n finalDisplayElement &&\n createPortal(\n \n {finalDisplayElement}\n ,\n document.body\n ),\n showClickBurst &&\n createPortal(\n \n \n ,\n document.body\n ),\n ];\n\n setPortalElements((current) => ({\n ...current,\n ...items.reduce(\n (acc, item, index) => ({\n ...acc,\n [`${id}_${index}`]: item,\n }),\n {}\n ),\n }));\n setTimeout(() => {\n setPortalElements((current) => {\n const newState = { ...current };\n items.forEach((_, index) => {\n delete newState[`${id}_${index}`];\n });\n return newState;\n });\n }, duration);\n },\n [containerStyle, displayElement]\n );\n\n const renderedPortalElements = useMemo(\n () => <>{Object.values(portalElements)},\n [portalElements]\n );\n\n return { domElement: renderedPortalElements, trigger };\n};\n", "type": "registry:ui" } ], "type": "registry:ui" }