diff --git a/apps/web/vibes/soul/docs/gift-card.mdx b/apps/web/vibes/soul/docs/gift-card.mdx new file mode 100644 index 000000000..8577385a4 --- /dev/null +++ b/apps/web/vibes/soul/docs/gift-card.mdx @@ -0,0 +1,4 @@ +--- +title: Gift Card +preview: gift-card-example +--- diff --git a/apps/web/vibes/soul/examples.ts b/apps/web/vibes/soul/examples.ts index a11cc16a6..87485600f 100644 --- a/apps/web/vibes/soul/examples.ts +++ b/apps/web/vibes/soul/examples.ts @@ -1156,5 +1156,12 @@ export const examples = [ files: ['examples/primitives/scroll-area/luxury.tsx'], component: lazy(() => import('./examples/primitives/scroll-area/luxury')), }, + { + name: 'gift-card-example', + dependencies: [], + registryDependencies: ['gift-card'], + files: ['examples/primitives/gift-card/index.tsx'], + component: lazy(() => import('./examples/primitives/gift-card')), + }, // PLOP: Append new component here ] satisfies Components; diff --git a/apps/web/vibes/soul/examples/primitives/gift-card/index.tsx b/apps/web/vibes/soul/examples/primitives/gift-card/index.tsx new file mode 100644 index 000000000..8b419b475 --- /dev/null +++ b/apps/web/vibes/soul/examples/primitives/gift-card/index.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { GiftCard } from '@/vibes/soul/primitives/gift-card'; + +export default function Preview() { + return ( +
+ +
+ ); +} diff --git a/apps/web/vibes/soul/navigation.ts b/apps/web/vibes/soul/navigation.ts index 6cb2aa6e4..83ea9b19c 100644 --- a/apps/web/vibes/soul/navigation.ts +++ b/apps/web/vibes/soul/navigation.ts @@ -90,6 +90,7 @@ const primitivePages: [Page, ...Page[]] = [ component: 'dropdown-menu', }, { title: 'Favorite', slug: 'favorite', file: 'docs/favorite.mdx', component: 'favorite' }, + { title: 'Gift Card', slug: 'gift-card', file: 'docs/gift-card.mdx', component: 'gift-card' }, { title: 'Icon', slug: 'icon', file: 'docs/icon.mdx', component: 'icon' }, { title: 'Logo', diff --git a/apps/web/vibes/soul/primitives/gift-card/index.tsx b/apps/web/vibes/soul/primitives/gift-card/index.tsx new file mode 100644 index 000000000..6c456e985 --- /dev/null +++ b/apps/web/vibes/soul/primitives/gift-card/index.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { useMemo, useRef, useState } from 'react'; + +export interface GiftCardProps { + maxTilt?: number; + amount: string | null; + logo: string; + loading?: boolean; +} + +export function GiftCard({ + maxTilt = 15, + amount = '$25.00', + logo = 'Planted', + loading = false, +}: GiftCardProps) { + const cardRef = useRef(null); + + const [mouse, setMouse] = useState({ x: 0, y: 0, xPct: 50, yPct: 50 }); + const [tilt, setTilt] = useState({ rotateX: 0, rotateY: 0, z: 0 }); + + // Derived background positions for holo layers + const bgX = useMemo(() => 40 + 0.2 * mouse.xPct, [mouse.xPct]); + const bgY = useMemo(() => 40 + 0.2 * mouse.yPct, [mouse.yPct]); + + function handleMouseMove(e: React.MouseEvent) { + const target = e.currentTarget; + const rect = target.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const xPct = (x / rect.width) * 100; + const yPct = (y / rect.height) * 100; + setMouse({ x, y, xPct, yPct }); + } + + function handlePointerMove(e: React.PointerEvent) { + const el = cardRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const xPercent = x / rect.width; + const yPercent = y / rect.height; + setTilt({ + rotateX: maxTilt * (0.5 - yPercent), + rotateY: maxTilt * (xPercent - 0.5), + z: -10, + }); + } + + function handlePointerEnter() { + setTilt((t) => ({ ...t, z: -10 })); + } + + function handlePointerLeave() { + setTilt({ rotateX: 0, rotateY: 0, z: 0 }); + setMouse({ x: 0, y: 0, xPct: 50, yPct: 50 }); + } + + return ( +
+
+ {/* Left column */} +
+

+ {logo} +

+

+ Gift card +

+
+ + {/* Right column */} +
+ {amount !== null && !loading && ( +

+ {amount} +

+ )} + {loading &&
} +
+ + {/* Cursor mask */} +
+ + {/* Holographic overlay */} +
+
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31bf143b2..186b00f4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: version: 3.1.0(react@19.0.0)(types-react@19.0.0-rc.1) '@nextui-org/date-input': specifier: ^2.1.3 - version: 2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-accordion': specifier: 1.2.1 version: 1.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) @@ -3915,8 +3915,8 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - framer-motion@12.4.7: - resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==} + framer-motion@12.23.22: + resolution: {integrity: sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -4935,11 +4935,11 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} - motion-dom@12.4.10: - resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==} + motion-dom@12.23.21: + resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} - motion-utils@12.4.10: - resolution: {integrity: sha512-NPwZd94V013SwRf++jMrk2+HEBgPkeIE2RiOzhAuuQlqxMJPkKt/LXVh6Upl+iN8oarSGD2dlY5/bqgsYXDABA==} + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} @@ -7486,13 +7486,13 @@ snapshots: '@next/swc-win32-x64-msvc@15.1.7': optional: true - '@nextui-org/date-input@2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@nextui-org/date-input@2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@internationalized/date': 3.6.0 - '@nextui-org/form': 2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@nextui-org/form': 2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@nextui-org/react-utils': 2.1.3(react@19.0.0) '@nextui-org/shared-utils': 2.1.2 - '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@nextui-org/theme': 2.4.5(tailwindcss@4.1.5) '@react-aria/datepicker': 3.12.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@react-aria/i18n': 3.12.4(react@19.0.0) @@ -7503,11 +7503,11 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@nextui-org/form@2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@nextui-org/form@2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@nextui-org/react-utils': 2.1.3(react@19.0.0) '@nextui-org/shared-utils': 2.1.2 - '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@nextui-org/theme': 2.4.5(tailwindcss@4.1.5) '@react-aria/utils': 3.26.0(react@19.0.0) '@react-stately/form': 3.1.0(react@19.0.0) @@ -7535,7 +7535,7 @@ snapshots: clsx: 1.2.1 react: 19.0.0 - '@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@internationalized/date': 3.6.0 '@nextui-org/react-utils': 2.1.3(react@19.0.0) @@ -7545,7 +7545,7 @@ snapshots: '@react-aria/utils': 3.26.0(react@19.0.0) '@react-stately/utils': 3.10.5(react@19.0.0) '@react-types/datepicker': 3.9.0(react@19.0.0) - framer-motion: 12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + framer-motion: 12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) transitivePeerDependencies: @@ -10471,10 +10471,10 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - motion-dom: 12.4.10 - motion-utils: 12.4.10 + motion-dom: 12.23.21 + motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: react: 19.0.0 @@ -11821,11 +11821,11 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - motion-dom@12.4.10: + motion-dom@12.23.21: dependencies: - motion-utils: 12.4.10 + motion-utils: 12.23.6 - motion-utils@12.4.10: {} + motion-utils@12.23.6: {} mri@1.2.0: {}