From 27ff3810a4ebcb9a06a97243d07a2d35b828c90c Mon Sep 17 00:00:00 2001 From: "Isaac \"H3rmel\" Reginato" <97918507+h3rmel@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:43:17 -0300 Subject: [PATCH] Feature (Components): New Pointer component (#544) * feat: add Pointer component to registry and documentation * fix: format and lint pointer registry and demo files * fix: update pointer demo * fix: bump * fix: update registry * fix: update date --------- Co-authored-by: Dillion Verma --- __registry__/index.tsx | 47 ++++++++++ config/docs.ts | 6 ++ content/docs/components/pointer.mdx | 46 ++++++++++ public/r/pointer-demo-1.json | 18 ++++ public/r/pointer.json | 18 ++++ registry.json | 32 +++++++ registry/example/pointer-demo-1.tsx | 15 +++ registry/magicui/pointer.tsx | 136 ++++++++++++++++++++++++++++ registry/registry-examples.ts | 14 +++ registry/registry-ui.ts | 15 +++ 10 files changed, 347 insertions(+) create mode 100644 content/docs/components/pointer.mdx create mode 100644 public/r/pointer-demo-1.json create mode 100644 public/r/pointer.json create mode 100644 registry/example/pointer-demo-1.tsx create mode 100644 registry/magicui/pointer.tsx diff --git a/__registry__/index.tsx b/__registry__/index.tsx index 561f533f9..587317867 100644 --- a/__registry__/index.tsx +++ b/__registry__/index.tsx @@ -201,6 +201,30 @@ export const Index: Record = { }), meta: undefined, }, + pointer: { + name: "pointer", + description: + "A component that displays a pointer when hovering over an element", + type: "registry:ui", + registryDependencies: undefined, + files: [ + { + path: "registry/magicui/pointer.tsx", + type: "registry:ui", + target: "components/magicui/pointer.tsx", + }, + ], + component: React.lazy(async () => { + const mod = await import("@/registry/magicui/pointer.tsx"); + const exportName = + Object.keys(mod).find( + (key) => + typeof mod[key] === "function" || typeof mod[key] === "object", + ) || item.name; + return { default: mod.default || mod[exportName] }; + }), + meta: undefined, + }, "neon-gradient-card": { name: "neon-gradient-card", description: "A beautiful neon card effect", @@ -1784,6 +1808,29 @@ export const Index: Record = { }), meta: undefined, }, + "pointer-demo-1": { + name: "pointer-demo-1", + description: "Example showing a pointer effect component", + type: "registry:example", + registryDependencies: ["https://magicui.design/r/pointer"], + files: [ + { + path: "registry/example/pointer-demo-1.tsx", + type: "registry:example", + target: "components/pointer-demo-1.tsx", + }, + ], + component: React.lazy(async () => { + const mod = await import("@/registry/example/pointer-demo-1.tsx"); + const exportName = + Object.keys(mod).find( + (key) => + typeof mod[key] === "function" || typeof mod[key] === "object", + ) || item.name; + return { default: mod.default || mod[exportName] }; + }), + meta: undefined, + }, "neon-gradient-card-demo": { name: "neon-gradient-card-demo", description: "Example showing a beautiful neon card effect.", diff --git a/config/docs.ts b/config/docs.ts index 787e2ceb7..edf31e66d 100644 --- a/config/docs.ts +++ b/config/docs.ts @@ -225,6 +225,12 @@ export const docsConfig: DocsConfig = { items: [], label: "New", }, + { + title: "Pointer", + href: `/docs/components/pointer`, + items: [], + label: "New", + }, ], }, { diff --git a/content/docs/components/pointer.mdx b/content/docs/components/pointer.mdx new file mode 100644 index 000000000..d863fa2e6 --- /dev/null +++ b/content/docs/components/pointer.mdx @@ -0,0 +1,46 @@ +--- +title: Pointer +date: 2025-02-17 +description: A component that displays a pointer when hovering over an element +author: h3rmel +published: true +--- + + + +## Installation + + + + + CLI + Manual + + + +```bash +npx shadcn@latest add "https://magicui.design/r/pointer" +``` + + + + + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + + + + + +## Examples + +## Props + +| Property | Type | Default | Description | +| ----------- | ----------------- | ------- | ----------------------------------------------- | +| `children` | `React.ReactNode` | - | The content that will be wrapped by the pointer | +| `className` | `string` | - | The className of the pointer | diff --git a/public/r/pointer-demo-1.json b/public/r/pointer-demo-1.json new file mode 100644 index 000000000..d4238e7c5 --- /dev/null +++ b/public/r/pointer-demo-1.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "pointer-demo-1", + "type": "registry:example", + "title": "Pointer Demo 1", + "description": "Example showing a pointer effect component", + "registryDependencies": [ + "https://magicui.design/r/pointer" + ], + "files": [ + { + "path": "registry/example/pointer-demo-1.tsx", + "content": "\"use client\";\n\nimport { PointerWrapper } from \"@/components/magicui/pointer\";\n\nexport default function PointerDemo1() {\n return (\n \n
\n \n Pointer\n \n
\n
\n );\n}\n", + "type": "registry:example", + "target": "components/pointer-demo-1.tsx" + } + ] +} \ No newline at end of file diff --git a/public/r/pointer.json b/public/r/pointer.json new file mode 100644 index 000000000..a5adc2cde --- /dev/null +++ b/public/r/pointer.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "pointer", + "type": "registry:ui", + "title": "Pointer", + "description": "A component that displays a pointer when hovering over an element", + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "registry/magicui/pointer.tsx", + "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useMotionValue } from \"motion/react\";\nimport React, { useEffect, useRef, useState } from \"react\";\n\n/**\n * @property {React.ReactNode} children - The child elements to be wrapped\n */\ninterface PointerWrapperProps extends React.HTMLAttributes {\n children?: React.ReactNode;\n}\n\n/**\n * A component that wraps content and adds a custom pointer animation when hovering\n * over the wrapped area. The pointer follows the mouse movement within the wrapped area.\n *\n * @component\n * @param {PointerWrapperProps} props - The component props\n */\nexport function PointerWrapper({\n children,\n className,\n ...props\n}: PointerWrapperProps) {\n const x = useMotionValue(0);\n const y = useMotionValue(0);\n\n const ref = useRef(null);\n const [rect, setRect] = useState(null);\n const [isInside, setIsInside] = useState(false);\n\n useEffect(() => {\n function updateRect() {\n if (ref.current) {\n setRect(ref.current.getBoundingClientRect());\n }\n }\n\n // Initial rect calculation\n updateRect();\n\n // Update rect on window resize\n window.addEventListener(\"resize\", updateRect);\n\n return () => {\n window.removeEventListener(\"resize\", updateRect);\n };\n }, []);\n\n function handleMouseMove(e: React.MouseEvent) {\n if (rect) {\n x.set(e.clientX - rect.left);\n y.set(e.clientY - rect.top);\n }\n }\n\n function handleMouseLeave() {\n setIsInside(false);\n }\n\n function handleMouseEnter() {\n if (ref.current) {\n setRect(ref.current.getBoundingClientRect());\n setIsInside(true);\n }\n }\n\n return (\n \n {isInside && }\n {children}\n \n );\n}\n\n/**\n * @property {MotionValue} x - The x-coordinate position of the pointer\n * @property {MotionValue} y - The y-coordinate position of the pointer\n */\ninterface PointerProps {\n x: any;\n y: any;\n}\n\n/**\n * A custom pointer component that displays an animated arrow cursor\n *\n * @description Used internally by PointerWrapper to show the custom cursor\n *\n * @component\n * @param {PointerProps} props - The component props\n */\nfunction Pointer({ x, y }: PointerProps): JSX.Element {\n return (\n \n \n \n \n \n );\n}\n", + "type": "registry:ui", + "target": "components/magicui/pointer.tsx" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index d5e42107f..181d06b51 100644 --- a/registry.json +++ b/registry.json @@ -248,6 +248,22 @@ } ] }, + { + "name": "pointer", + "type": "registry:ui", + "title": "Pointer", + "description": "A component that displays a pointer when hovering over an element", + "dependencies": [ + "motion" + ], + "files": [ + { + "path": "registry/magicui/pointer.tsx", + "type": "registry:ui", + "target": "components/magicui/pointer.tsx" + } + ] + }, { "name": "neon-gradient-card", "type": "registry:ui", @@ -1577,6 +1593,22 @@ } ] }, + { + "name": "pointer-demo-1", + "type": "registry:example", + "title": "Pointer Demo 1", + "description": "Example showing a pointer effect component", + "registryDependencies": [ + "https://magicui.design/r/pointer" + ], + "files": [ + { + "path": "registry/example/pointer-demo-1.tsx", + "type": "registry:example", + "target": "components/pointer-demo-1.tsx" + } + ] + }, { "name": "neon-gradient-card-demo", "type": "registry:example", diff --git a/registry/example/pointer-demo-1.tsx b/registry/example/pointer-demo-1.tsx new file mode 100644 index 000000000..bccc548d0 --- /dev/null +++ b/registry/example/pointer-demo-1.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { PointerWrapper } from "@/registry/magicui/pointer"; + +export default function PointerDemo1() { + return ( + +
+ + Pointer + +
+
+ ); +} diff --git a/registry/magicui/pointer.tsx b/registry/magicui/pointer.tsx new file mode 100644 index 000000000..5abb1cc11 --- /dev/null +++ b/registry/magicui/pointer.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { AnimatePresence, motion, useMotionValue } from "motion/react"; +import React, { useEffect, useRef, useState } from "react"; + +/** + * @property {React.ReactNode} children - The child elements to be wrapped + */ +interface PointerWrapperProps extends React.HTMLAttributes { + children?: React.ReactNode; +} + +/** + * A component that wraps content and adds a custom pointer animation when hovering + * over the wrapped area. The pointer follows the mouse movement within the wrapped area. + * + * @component + * @param {PointerWrapperProps} props - The component props + */ +export function PointerWrapper({ + children, + className, + ...props +}: PointerWrapperProps) { + const x = useMotionValue(0); + const y = useMotionValue(0); + + const ref = useRef(null); + const [rect, setRect] = useState(null); + const [isInside, setIsInside] = useState(false); + + useEffect(() => { + function updateRect() { + if (ref.current) { + setRect(ref.current.getBoundingClientRect()); + } + } + + // Initial rect calculation + updateRect(); + + // Update rect on window resize + window.addEventListener("resize", updateRect); + + return () => { + window.removeEventListener("resize", updateRect); + }; + }, []); + + function handleMouseMove(e: React.MouseEvent) { + if (rect) { + x.set(e.clientX - rect.left); + y.set(e.clientY - rect.top); + } + } + + function handleMouseLeave() { + setIsInside(false); + } + + function handleMouseEnter() { + if (ref.current) { + setRect(ref.current.getBoundingClientRect()); + setIsInside(true); + } + } + + return ( +
+ {isInside && } + {children} +
+ ); +} + +/** + * @property {MotionValue} x - The x-coordinate position of the pointer + * @property {MotionValue} y - The y-coordinate position of the pointer + */ +interface PointerProps { + x: any; + y: any; +} + +/** + * A custom pointer component that displays an animated arrow cursor + * + * @description Used internally by PointerWrapper to show the custom cursor + * + * @component + * @param {PointerProps} props - The component props + */ +function Pointer({ x, y }: PointerProps): JSX.Element { + return ( + + + + + + ); +} diff --git a/registry/registry-examples.ts b/registry/registry-examples.ts index 7ea1f402a..b0e7db370 100644 --- a/registry/registry-examples.ts +++ b/registry/registry-examples.ts @@ -171,6 +171,20 @@ export const examples: Registry["items"] = [ }, ], }, + { + name: "pointer-demo-1", + type: "registry:example", + title: "Pointer Demo 1", + description: "Example showing a pointer effect component", + registryDependencies: ["https://magicui.design/r/pointer"], + files: [ + { + path: "registry/example/pointer-demo-1.tsx", + type: "registry:example", + target: "components/pointer-demo-1.tsx", + }, + ], + }, { name: "neon-gradient-card-demo", type: "registry:example", diff --git a/registry/registry-ui.ts b/registry/registry-ui.ts index 44820eb33..7c61e816e 100644 --- a/registry/registry-ui.ts +++ b/registry/registry-ui.ts @@ -174,6 +174,21 @@ export const ui: Registry["items"] = [ }, ], }, + { + name: "pointer", + type: "registry:ui", + title: "Pointer", + description: + "A component that displays a pointer when hovering over an element", + dependencies: ["motion"], + files: [ + { + path: "registry/magicui/pointer.tsx", + type: "registry:ui", + target: "components/magicui/pointer.tsx", + }, + ], + }, { name: "neon-gradient-card", type: "registry:ui",