From be55ec13db1c0d8b6538bde337d0c68938809049 Mon Sep 17 00:00:00 2001 From: dvelo Date: Mon, 21 Apr 2025 23:25:26 -0500 Subject: [PATCH] feat: finish home page --- apps/www/package.json | 1 + apps/www/src/app/globals.css | 25 ++- .../www/src/components/feat/footer/footer.tsx | 187 ++++++++-------- .../feat/home-page/animated-list.tsx | 2 +- .../feat/home-page/flickering-grid.tsx | 199 ++++++++++++++++++ .../components/feat/home-page/flip-text.tsx | 64 ++++++ .../feat/home-page/font-changer.tsx | 105 +++++++++ .../components/feat/home-page/home-page.tsx | 172 +++++++++++++-- .../home-page/interactive-hover-button.tsx | 35 +++ .../feat/home-page/line-shadow-text.tsx | 42 ++++ .../www/src/components/feat/navbar/navbar.tsx | 5 +- apps/www/src/components/ui/discord.tsx | 47 +++++ yarn.lock | 21 -- 13 files changed, 773 insertions(+), 132 deletions(-) create mode 100644 apps/www/src/components/feat/home-page/flickering-grid.tsx create mode 100644 apps/www/src/components/feat/home-page/flip-text.tsx create mode 100644 apps/www/src/components/feat/home-page/font-changer.tsx create mode 100644 apps/www/src/components/feat/home-page/interactive-hover-button.tsx create mode 100644 apps/www/src/components/feat/home-page/line-shadow-text.tsx create mode 100644 apps/www/src/components/ui/discord.tsx diff --git a/apps/www/package.json b/apps/www/package.json index 791543e..7faa849 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -57,6 +57,7 @@ "contentlayer": "^0.3.4", "cron": "^3.1.7", "discord.js": "^14.15.3", + "framer-motion": "^12.7.4", "github-slugger": "^2.0.0", "inngest": "^3.21.2", "input-otp": "^1.2.4", diff --git a/apps/www/src/app/globals.css b/apps/www/src/app/globals.css index 14597c9..642cf71 100644 --- a/apps/www/src/app/globals.css +++ b/apps/www/src/app/globals.css @@ -9354,6 +9354,15 @@ body { transform: rotate(-5deg) scale(0.9); } } + +@keyframes ripple { + 0%, 100% { + transform: translate(-50%, -50%) scale(1); + } + 50% { + transform: translate(-50%, -50%) scale(0.9); + } +} /* ---break--- */ @@ -9368,9 +9377,15 @@ body { --color-sidebar-ring: var(--sidebar-ring); --animate-aurora: aurora 8s ease-in-out infinite alternate; --animate-ripple: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite; - @keyframes ripple { - 0%, 100% { - transform: translate(-50%, -50%) scale(1);} - 50% { - transform: translate(-50%, -50%) scale(0.9);}} + + --animate-line-shadow: line-shadow 15s linear infinite; + + @keyframes line-shadow { + 0% { + background-position: 0 0; + } + 100% { + background-position: 100% -100%; + } + } } \ No newline at end of file diff --git a/apps/www/src/components/feat/footer/footer.tsx b/apps/www/src/components/feat/footer/footer.tsx index 3fddd15..a21fd32 100644 --- a/apps/www/src/components/feat/footer/footer.tsx +++ b/apps/www/src/components/feat/footer/footer.tsx @@ -5,100 +5,107 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { Button } from "@/components/ui/button"; import Github from "@/components/ui/github"; import Image from "next/image" +import { usePathname } from "next/navigation"; + +const hideFooterPages = ["/home"]; export function Footer() { - return ( - - ); + + + MHSF is an open-source project licensed under the MIT license. MHSF is + not officially affiliated with with Minehut, Super League Enterprise, + or GamerSafer in any way.
+ Spamming, abusing or misusing the Minehut API and/or MHSF will get + your IP blocked, we are not responsible for IP blocks.{" "} + You have been warned. +
+ If you're worried, please review the{" "} + + Rules + + , ToS &{" "} + Platform Rules. +
+
+ + ); + + return null; } diff --git a/apps/www/src/components/feat/home-page/animated-list.tsx b/apps/www/src/components/feat/home-page/animated-list.tsx index 0befeff..27ec441 100644 --- a/apps/www/src/components/feat/home-page/animated-list.tsx +++ b/apps/www/src/components/feat/home-page/animated-list.tsx @@ -1,7 +1,7 @@ "use client"; import { cn } from "@/lib/utils"; -import { AnimatePresence, motion } from "motion/react"; +import { AnimatePresence, motion } from "framer-motion"; import React, { type ComponentPropsWithoutRef, useEffect, diff --git a/apps/www/src/components/feat/home-page/flickering-grid.tsx b/apps/www/src/components/feat/home-page/flickering-grid.tsx new file mode 100644 index 0000000..f303d4d --- /dev/null +++ b/apps/www/src/components/feat/home-page/flickering-grid.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +interface FlickeringGridProps extends React.HTMLAttributes { + squareSize?: number; + gridGap?: number; + flickerChance?: number; + color?: string; + width?: number; + height?: number; + className?: string; + maxOpacity?: number; +} + +export const FlickeringGrid: React.FC = ({ + squareSize = 4, + gridGap = 6, + flickerChance = 0.3, + color = "rgb(0, 0, 0)", + width, + height, + className, + maxOpacity = 0.3, + ...props +}) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [isInView, setIsInView] = useState(false); + const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); + + const memoizedColor = useMemo(() => { + const toRGBA = (color: string) => { + if (typeof window === "undefined") { + return `rgba(0, 0, 0,`; + } + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = 1; + const ctx = canvas.getContext("2d"); + if (!ctx) return "rgba(255, 0, 0,"; + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data); + return `rgba(${r}, ${g}, ${b},`; + }; + return toRGBA(color); + }, [color]); + + const setupCanvas = useCallback( + (canvas: HTMLCanvasElement, width: number, height: number) => { + const dpr = window.devicePixelRatio || 1; + canvas.width = width * dpr; + canvas.height = height * dpr; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + const cols = Math.floor(width / (squareSize + gridGap)); + const rows = Math.floor(height / (squareSize + gridGap)); + + const squares = new Float32Array(cols * rows); + for (let i = 0; i < squares.length; i++) { + squares[i] = Math.random() * maxOpacity; + } + + return { cols, rows, squares, dpr }; + }, + [squareSize, gridGap, maxOpacity], + ); + + const updateSquares = useCallback( + (squares: Float32Array, deltaTime: number) => { + for (let i = 0; i < squares.length; i++) { + if (Math.random() < flickerChance * deltaTime) { + squares[i] = Math.random() * maxOpacity; + } + } + }, + [flickerChance, maxOpacity], + ); + + const drawGrid = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + cols: number, + rows: number, + squares: Float32Array, + dpr: number, + ) => { + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = "transparent"; + ctx.fillRect(0, 0, width, height); + + for (let i = 0; i < cols; i++) { + for (let j = 0; j < rows; j++) { + const opacity = squares[i * rows + j]; + ctx.fillStyle = `${memoizedColor}${opacity})`; + ctx.fillRect( + i * (squareSize + gridGap) * dpr, + j * (squareSize + gridGap) * dpr, + squareSize * dpr, + squareSize * dpr, + ); + } + } + }, + [memoizedColor, squareSize, gridGap], + ); + + useEffect(() => { + const canvas = canvasRef.current; + const container = containerRef.current; + if (!canvas || !container) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animationFrameId: number; + let gridParams: ReturnType; + + const updateCanvasSize = () => { + const newWidth = width || container.clientWidth; + const newHeight = height || container.clientHeight; + setCanvasSize({ width: newWidth, height: newHeight }); + gridParams = setupCanvas(canvas, newWidth, newHeight); + }; + + updateCanvasSize(); + + let lastTime = 0; + const animate = (time: number) => { + if (!isInView) return; + + const deltaTime = (time - lastTime) / 1000; + lastTime = time; + + updateSquares(gridParams.squares, deltaTime); + drawGrid( + ctx, + canvas.width, + canvas.height, + gridParams.cols, + gridParams.rows, + gridParams.squares, + gridParams.dpr, + ); + animationFrameId = requestAnimationFrame(animate); + }; + + const resizeObserver = new ResizeObserver(() => { + updateCanvasSize(); + }); + + resizeObserver.observe(container); + + const intersectionObserver = new IntersectionObserver( + ([entry]) => { + setIsInView(entry.isIntersecting); + }, + { threshold: 0 }, + ); + + intersectionObserver.observe(canvas); + + if (isInView) { + animationFrameId = requestAnimationFrame(animate); + } + + return () => { + cancelAnimationFrame(animationFrameId); + resizeObserver.disconnect(); + intersectionObserver.disconnect(); + }; + }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]); + + return ( +
+ +
+ ); +}; diff --git a/apps/www/src/components/feat/home-page/flip-text.tsx b/apps/www/src/components/feat/home-page/flip-text.tsx new file mode 100644 index 0000000..4f4822a --- /dev/null +++ b/apps/www/src/components/feat/home-page/flip-text.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { AnimatePresence, motion, Variants, MotionProps } from "framer-motion"; + +import { cn } from "@/lib/utils"; +import { ElementType } from "react"; +import React from "react"; + +interface FlipTextProps extends MotionProps { + /** The duration of the animation */ + duration?: number; + /** The delay between each character */ + delayMultiple?: number; + /** The variants of the animation */ + framerProps?: Variants; + /** The class name of the component */ + className?: string; + /** The element type of the component */ + as?: ElementType; + /** The children of the component */ + children: React.ReactNode; + /** The variants of the animation */ + variants?: Variants; +} + +const defaultVariants: Variants = { + hidden: { rotateX: -90, opacity: 0 }, + visible: { rotateX: 0, opacity: 1 }, +}; + +export function FlipText({ + children, + duration = 0.5, + delayMultiple = 0.08, + + className, + as: Component = "span", + variants, + ...props +}: FlipTextProps) { + const MotionComponent = motion.create(Component); + const characters = React.Children.toArray(children).join("").split(""); + + return ( +
+ + {characters.map((char, i) => ( + + {char} + + ))} + +
+ ); +} diff --git a/apps/www/src/components/feat/home-page/font-changer.tsx b/apps/www/src/components/feat/home-page/font-changer.tsx new file mode 100644 index 0000000..bcd42fb --- /dev/null +++ b/apps/www/src/components/feat/home-page/font-changer.tsx @@ -0,0 +1,105 @@ +/* + * MHSF, Minehut Server List + * All external content is rather licensed under the ECA Agreement + * located here: https://mhsf.app/docs/legal/external-content-agreement + * + * All code under MHSF is licensed under the MIT License + * by open source contributors + * + * Copyright (c) 2025 dvelo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +import { useEffectOnce } from "@/lib/useEffectOnce"; +import { + Inter, + Roboto, + Montserrat, + Ubuntu_Condensed, + Ubuntu, + Poppins, +} from "next/font/google"; +import { GeistSans } from "geist/font/sans"; +import { useEffect, useMemo, useState, type JSX } from "react"; +import { cn } from "@/lib/utils"; + +const inter = Inter({ subsets: ["latin"] }); +const roboto = Roboto({ + subsets: ["latin"], + weight: ["100", "300", "400", "500", "700", "900"], +}); +const montserrat = Montserrat({ + subsets: ["latin"], + weight: ["100", "300", "400", "500", "700", "900"], +}); +const ubuntuCondensed = Ubuntu_Condensed({ + subsets: ["latin"], + weight: ["400"], +}); +const ubuntu = Ubuntu({ + subsets: ["latin"], + weight: ["400"], +}); +const poppins = Poppins({ + subsets: ["latin"], + weight: ["400"], +}); + +const fonts = [ + inter.style, + roboto.style, + montserrat.style, + GeistSans.style, + ubuntuCondensed.style, + ubuntu.style, + poppins.style, +]; + +export function FontChanger({ + children, + nounderline +}: { children: JSX.Element | JSX.Element[] | string, nounderline?: boolean }) { + const [position, setPosition] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setPosition((position) => { + if (position === fonts.length - 1) return 0; + return position + 1; + }); + }, 1000); + + return () => clearInterval(interval); + }, []); + + console.log(position); + + return ( +
+ + {children} + +
+ ); +} diff --git a/apps/www/src/components/feat/home-page/home-page.tsx b/apps/www/src/components/feat/home-page/home-page.tsx index 18abaa5..a563752 100644 --- a/apps/www/src/components/feat/home-page/home-page.tsx +++ b/apps/www/src/components/feat/home-page/home-page.tsx @@ -47,6 +47,13 @@ import { ExampleChart } from "./example-chart"; import { Link } from "@/components/util/link"; import { type Avatar, AvatarCircles } from "./avatar-circles"; import { Ripple } from "./ripple"; +import { FontChanger } from "./font-changer"; +import { FlickeringGrid } from "./flickering-grid"; +import { DiscordWordmark } from "@/components/ui/discord"; +import { Separator } from "@/components/ui/separator"; +import { LineShadowText } from "@/components/feat/home-page/line-shadow-text"; +import { FlipText } from "./flip-text"; +import { InteractiveHoverButton } from "./interactive-hover-button"; const getGitHubDetails = async () => { const githubRepo = await ( @@ -71,6 +78,7 @@ export default function HomePageComponent() { const router = useRouter(); const { isSignedIn } = useUser(); const theme = useTheme(); + const shadowColor = theme.resolvedTheme === "dark" ? "white" : "black"; const { resolvedTheme } = useTheme(); const [stars, setStars] = useState(0); const [stargazers, setStargazers] = useState([]); @@ -117,7 +125,10 @@ export default function HomePageComponent() {

- The missing half of Minehut + The missing half of{" "} + + Minehut +

MHSF is the next generation Minehut server list wrapper, with
@@ -150,8 +161,18 @@ export default function HomePageComponent() {


-

- An open-source unofficial project brought to you by dvelo +

+ An open-source unofficial project brought to you by{" "} + + + dvelo +

@@ -228,10 +249,7 @@ export default function HomePageComponent() { .reverse() .flat() .map((c) => ( - + ))} @@ -306,10 +324,10 @@ export default function HomePageComponent() { entries.

-
- -
-
+
+ +
+
@@ -329,17 +347,145 @@ export default function HomePageComponent() { -
+
- +
For server owners
+
+ +

+ Showcase your server +

+

+ Make your server stand out from the crowd with options to
+ display what your server is. +

+
+
+
+
+
+ + + + + Show. Not tell. + +

+ Use a static banner that can show what your server is about. +

+
+
+ +
+ + + + + 300k+ online currently + + +
+ + + + +
+
+ + + Link your community. + +

+ Quickly enable a embed of your Discord server to show + directly on your server page. +

+
+
+ + + + + + + My Server + + + Lorem ipsum dolor sit amet consectetur adipiscing elit. + Consectetur adipiscing elit quisque faucibus ex sapien + vitae. + + + + + + Whats your server? + +

+ Describe your server using Markdown to include bold, italic + & images directly in the description of your server. +

+
+
+
+
+
+ +
+ +

+ + The modern server list + +

+

+ Whether you are a player, data hunter or server owner, MHSF{" "} +
+ always has a solution to the community side of Minehut. +

+ + Find servers + +
+
+ +
+
+ +
); } diff --git a/apps/www/src/components/feat/home-page/interactive-hover-button.tsx b/apps/www/src/components/feat/home-page/interactive-hover-button.tsx new file mode 100644 index 0000000..608b32e --- /dev/null +++ b/apps/www/src/components/feat/home-page/interactive-hover-button.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { ArrowRightIcon } from "@radix-ui/react-icons"; + +interface InteractiveHoverButtonProps + extends React.ButtonHTMLAttributes {} + +export const InteractiveHoverButton = React.forwardRef< + HTMLButtonElement, + InteractiveHoverButtonProps +>(({ children, className, ...props }, ref) => { + return ( + + ); +}); + +InteractiveHoverButton.displayName = "InteractiveHoverButton"; diff --git a/apps/www/src/components/feat/home-page/line-shadow-text.tsx b/apps/www/src/components/feat/home-page/line-shadow-text.tsx new file mode 100644 index 0000000..789178d --- /dev/null +++ b/apps/www/src/components/feat/home-page/line-shadow-text.tsx @@ -0,0 +1,42 @@ +import { cn } from "@/lib/utils"; +import { motion, type MotionProps } from "framer-motion"; + +interface LineShadowTextProps + extends Omit, keyof MotionProps>, + MotionProps { + shadowColor?: string; + as?: React.ElementType; +} + +export function LineShadowText({ + children, + shadowColor = "black", + className, + as: Component = "span", + ...props +}: LineShadowTextProps) { + const MotionComponent = motion.create(Component); + const content = typeof children === "string" ? children : null; + + if (!content) { + throw new Error("LineShadowText only accepts string content"); + } + + return ( + + {content} + + ); +} diff --git a/apps/www/src/components/feat/navbar/navbar.tsx b/apps/www/src/components/feat/navbar/navbar.tsx index 0c11265..e919d47 100644 --- a/apps/www/src/components/feat/navbar/navbar.tsx +++ b/apps/www/src/components/feat/navbar/navbar.tsx @@ -73,7 +73,8 @@ export function NavBar() { className={cn( "w-screen h-[3rem] grid-cols-3 fixed z-10 flex", "items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between", - showBorder ? "border-b backdrop-blur-xl" : "", + "lg:top-0 max-lg:bottom-0", + showBorder ? "border-b backdrop-blur-xl" : "max-lg:border-b max-lg:backdrop-blur-xl", pathname !== null && animatedTopbarPages.includes(pathname) ? "[--animation-delay:1000ms] opacity-0 animate-fade-in" : "" @@ -187,7 +188,7 @@ export function NavBar() {
diff --git a/apps/www/src/components/ui/discord.tsx b/apps/www/src/components/ui/discord.tsx new file mode 100644 index 0000000..c6ee054 --- /dev/null +++ b/apps/www/src/components/ui/discord.tsx @@ -0,0 +1,47 @@ +/* + * MHSF, Minehut Server List + * All external content is rather licensed under the ECA Agreement + * located here: https://mhsf.app/docs/legal/external-content-agreement + * + * All code under MHSF is licensed under the MIT License + * by open source contributors + * + * Copyright (c) 2025 dvelo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +import type { SVGProps } from "react"; + +export function DiscordWordmark(props: SVGProps) { + return ( + + ); +} diff --git a/yarn.lock b/yarn.lock index 2918e1e..d4ba2af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6456,15 +6456,6 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -framer-motion@^11.3.8: - version "11.18.2" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718" - integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w== - dependencies: - motion-dom "^11.18.1" - motion-utils "^11.18.1" - tslib "^2.4.0" - framer-motion@^12.7.4: version "12.7.4" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.7.4.tgz#50aeb8b5b5a672dea931bdb74956d7b526bf0b4b" @@ -9495,13 +9486,6 @@ mongodb@^6.12.0, mongodb@^6.8.0: bson "^6.10.3" mongodb-connection-string-url "^3.0.0" -motion-dom@^11.18.1: - version "11.18.1" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2" - integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw== - dependencies: - motion-utils "^11.18.1" - motion-dom@^12.7.4: version "12.7.4" resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.7.4.tgz#80f5f8d706e94bc29f6f4f4afa300ff9e1f976a2" @@ -9509,11 +9493,6 @@ motion-dom@^12.7.4: dependencies: motion-utils "^12.7.2" -motion-utils@^11.18.1: - version "11.18.1" - resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b" - integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA== - motion-utils@^12.7.2: version "12.7.2" resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.7.2.tgz#99b673d8851583b325bd0c8b0f04c5bf42b9b818"