mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
1 line
24 KiB
JSON
1 line
24 KiB
JSON
{"name":"StaggeredMenu","title":"StaggeredMenu","description":"Menu with staggered item animations and smooth transitions on open/close.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div class=\"w-full h-full sm-scope\">\n <div\n :class=\"(className ? className + ' ' : '') + 'staggered-menu-wrapper relative w-full h-full z-40'\"\n :style=\"accentColor ? { '--sm-accent': accentColor } : undefined\"\n :data-position=\"position\"\n :data-open=\"open || undefined\"\n >\n <div\n ref=\"preLayersRef\"\n class=\"top-0 right-0 bottom-0 z-[5] absolute pointer-events-none sm-prelayers\"\n aria-hidden=\"true\"\n >\n <div\n v-for=\"(color, index) in processedColors\"\n :key=\"index\"\n class=\"top-0 right-0 absolute w-full h-full translate-x-0 sm-prelayer\"\n :style=\"{ background: color }\"\n />\n </div>\n\n <header\n class=\"top-0 left-0 z-20 absolute flex justify-between items-center bg-transparent p-[2em] w-full pointer-events-none staggered-menu-header\"\n aria-label=\"Main navigation header\"\n >\n <div class=\"flex items-center pointer-events-auto select-none sm-logo\" aria-label=\"Logo\">\n <img\n :src=\"logoUrl || '/src/assets/logos/reactbits-gh-white.svg'\"\n alt=\"Logo\"\n class=\"block w-auto h-8 object-contain sm-logo-img\"\n :draggable=\"false\"\n width=\"110\"\n height=\"24\"\n />\n </div>\n\n <button\n ref=\"toggleBtnRef\"\n class=\"inline-flex relative items-center gap-[0.3rem] bg-transparent border-0 overflow-visible font-medium text-[#e9e9ef] leading-none cursor-pointer pointer-events-auto sm-toggle\"\n :aria-label=\"open ? 'Close menu' : 'Open menu'\"\n :aria-expanded=\"open\"\n aria-controls=\"staggered-menu-panel\"\n @click=\"toggleMenu\"\n type=\"button\"\n >\n <span\n ref=\"textWrapRef\"\n class=\"inline-block relative w-[var(--sm-toggle-width,auto)] min-w-[var(--sm-toggle-width,auto)] h-[1em] overflow-hidden whitespace-nowrap sm-toggle-textWrap\"\n aria-hidden=\"true\"\n >\n <span ref=\"textInnerRef\" class=\"flex flex-col leading-none sm-toggle-textInner\">\n <span v-for=\"(line, index) in textLines\" :key=\"index\" class=\"block h-[1em] leading-none sm-toggle-line\">\n {{ line }}\n </span>\n </span>\n </span>\n\n <span\n ref=\"iconRef\"\n class=\"inline-flex relative justify-center items-center w-[14px] h-[14px] sm-icon shrink-0 [will-change:transform]\"\n aria-hidden=\"true\"\n >\n <span\n ref=\"plusHRef\"\n class=\"top-1/2 left-1/2 absolute bg-current rounded-[2px] w-full h-[2px] -translate-x-1/2 -translate-y-1/2 sm-icon-line [will-change:transform]\"\n />\n <span\n ref=\"plusVRef\"\n class=\"top-1/2 left-1/2 absolute bg-current rounded-[2px] w-full h-[2px] -translate-x-1/2 -translate-y-1/2 sm-icon-line sm-icon-line-v [will-change:transform]\"\n />\n </span>\n </button>\n </header>\n\n <aside\n id=\"staggered-menu-panel\"\n ref=\"panelRef\"\n class=\"top-0 right-0 z-10 absolute flex flex-col bg-white backdrop-blur-[12px] p-[6em_2em_2em_2em] h-full overflow-y-auto staggered-menu-panel\"\n style=\"webkit-backdrop-filter: blur(12px)\"\n :aria-hidden=\"!open\"\n >\n <div class=\"flex flex-col flex-1 gap-5 sm-panel-inner\">\n <ul\n class=\"flex flex-col gap-2 m-0 p-0 list-none sm-panel-list\"\n role=\"list\"\n :data-numbering=\"displayItemNumbering || undefined\"\n >\n <li\n v-if=\"items && items.length\"\n v-for=\"(item, idx) in items\"\n :key=\"item.label + idx\"\n class=\"relative overflow-hidden leading-none sm-panel-itemWrap\"\n >\n <a\n class=\"inline-block relative pr-[1.4em] font-semibold text-[4rem] text-black no-underline uppercase leading-none tracking-[-2px] transition-[background,color] duration-150 ease-linear cursor-pointer sm-panel-item\"\n :href=\"item.link\"\n :aria-label=\"item.ariaLabel\"\n :data-index=\"idx + 1\"\n >\n <span class=\"inline-block will-change-transform sm-panel-itemLabel [transform-origin:50%_100%]\">\n {{ item.label }}\n </span>\n </a>\n </li>\n <li v-else class=\"relative overflow-hidden leading-none sm-panel-itemWrap\" aria-hidden=\"true\">\n <span\n class=\"inline-block relative pr-[1.4em] font-semibold text-[4rem] text-black no-underline uppercase leading-none tracking-[-2px] transition-[background,color] duration-150 ease-linear cursor-pointer sm-panel-item\"\n >\n <span class=\"inline-block will-change-transform sm-panel-itemLabel [transform-origin:50%_100%]\">\n No items\n </span>\n </span>\n </li>\n </ul>\n\n <div\n v-if=\"displaySocials && socialItems && socialItems.length > 0\"\n class=\"flex flex-col gap-3 mt-auto pt-8 sm-socials\"\n aria-label=\"Social links\"\n >\n <h3 class=\"m-0 font-medium text-base sm-socials-title [color:var(--sm-accent,#ff0000)]\">Socials</h3>\n <ul class=\"flex flex-row flex-wrap items-center gap-4 m-0 p-0 list-none sm-socials-list\" role=\"list\">\n <li v-for=\"(social, i) in socialItems\" :key=\"social.label + i\" class=\"sm-socials-item\">\n <a\n :href=\"social.link\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"inline-block relative py-[2px] font-medium text-[#111] text-[1.2rem] no-underline transition-[color,opacity] duration-300 ease-linear sm-socials-link\"\n >\n {{ social.label }}\n </a>\n </li>\n </ul>\n </div>\n </div>\n </aside>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { gsap } from 'gsap';\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';\n\nexport interface StaggeredMenuItem {\n label: string;\n ariaLabel: string;\n link: string;\n}\nexport interface StaggeredMenuSocialItem {\n label: string;\n link: string;\n}\nexport interface StaggeredMenuProps {\n position?: 'left' | 'right';\n colors?: string[];\n items?: StaggeredMenuItem[];\n socialItems?: StaggeredMenuSocialItem[];\n displaySocials?: boolean;\n displayItemNumbering?: boolean;\n className?: string;\n logoUrl?: string;\n menuButtonColor?: string;\n openMenuButtonColor?: string;\n accentColor?: string;\n changeMenuColorOnOpen?: boolean;\n onMenuOpen?: () => void;\n onMenuClose?: () => void;\n}\n\nconst props = withDefaults(defineProps<StaggeredMenuProps>(), {\n position: 'right',\n colors: () => ['#9EF2B2', '#27FF64'],\n items: () => [],\n socialItems: () => [],\n displaySocials: true,\n displayItemNumbering: true,\n logoUrl: '/src/assets/logos/vuebits-gh-white.svg',\n menuButtonColor: '#fff',\n openMenuButtonColor: '#fff',\n changeMenuColorOnOpen: true,\n accentColor: '#27FF64'\n});\n\nconst open = ref(false);\nconst openRef = ref(false);\n\nconst panelRef = useTemplateRef('panelRef');\nconst preLayersRef = useTemplateRef('preLayersRef');\nconst preLayerElsRef = ref<HTMLElement[]>([]);\n\nconst plusHRef = useTemplateRef('plusHRef');\nconst plusVRef = useTemplateRef('plusVRef');\nconst iconRef = useTemplateRef('iconRef');\n\nconst textInnerRef = useTemplateRef('textInnerRef');\nconst textWrapRef = useTemplateRef('textWrapRef');\nconst textLines = ref<string[]>(['Menu', 'Close']);\n\nconst openTlRef = ref<gsap.core.Timeline | null>(null);\nconst closeTweenRef = ref<gsap.core.Tween | null>(null);\nconst spinTweenRef = ref<gsap.core.Timeline | null>(null);\nconst textCycleAnimRef = ref<gsap.core.Tween | null>(null);\nconst colorTweenRef = ref<gsap.core.Tween | null>(null);\n\nconst toggleBtnRef = useTemplateRef('toggleBtnRef');\nconst busyRef = ref(false);\n\nconst itemEntranceTweenRef = ref<gsap.core.Tween | null>(null);\n\nconst processedColors = computed(() => {\n const raw = props.colors && props.colors.length ? props.colors.slice(0, 4) : ['#20251F', '#353F37'];\n const arr = [...raw];\n if (arr.length >= 3) {\n const mid = Math.floor(arr.length / 2);\n arr.splice(mid, 1);\n }\n return arr;\n});\n\nlet gsapContext: gsap.Context | null = null;\n\nconst initializeGSAP = () => {\n gsapContext = gsap.context(() => {\n const panel = panelRef.value;\n const preContainer = preLayersRef.value;\n const plusH = plusHRef.value;\n const plusV = plusVRef.value;\n const icon = iconRef.value;\n const textInner = textInnerRef.value;\n\n if (!panel || !plusH || !plusV || !icon || !textInner) return;\n\n let preLayers: HTMLElement[] = [];\n if (preContainer) {\n preLayers = Array.from(preContainer.querySelectorAll('.sm-prelayer')) as HTMLElement[];\n }\n preLayerElsRef.value = preLayers;\n\n const offscreen = props.position === 'left' ? -100 : 100;\n gsap.set([panel, ...preLayers], { xPercent: offscreen });\n\n gsap.set(plusH, { transformOrigin: '50% 50%', rotate: 0 });\n gsap.set(plusV, { transformOrigin: '50% 50%', rotate: 90 });\n gsap.set(icon, { rotate: 0, transformOrigin: '50% 50%' });\n\n gsap.set(textInner, { yPercent: 0 });\n\n if (toggleBtnRef.value) {\n gsap.set(toggleBtnRef.value, { color: props.menuButtonColor });\n }\n });\n};\n\nconst buildOpenTimeline = (): gsap.core.Timeline | null => {\n const panel = panelRef.value;\n const layers = preLayerElsRef.value;\n if (!panel) return null;\n\n openTlRef.value?.kill();\n if (closeTweenRef.value) {\n closeTweenRef.value.kill();\n closeTweenRef.value = null;\n }\n itemEntranceTweenRef.value?.kill();\n\n const itemEls = Array.from(panel.querySelectorAll('.sm-panel-itemLabel')) as HTMLElement[];\n const numberEls = Array.from(\n panel.querySelectorAll('.sm-panel-list[data-numbering] .sm-panel-item')\n ) as HTMLElement[];\n const socialTitle = panel.querySelector('.sm-socials-title') as HTMLElement | null;\n const socialLinks = Array.from(panel.querySelectorAll('.sm-socials-link')) as HTMLElement[];\n\n const layerStates = layers.map((el: HTMLElement) => ({ el, start: Number(gsap.getProperty(el, 'xPercent')) }));\n const panelStart = Number(gsap.getProperty(panel, 'xPercent'));\n\n if (itemEls.length) gsap.set(itemEls, { yPercent: 140, rotate: 10 });\n if (numberEls.length) gsap.set(numberEls, { ['--sm-num-opacity' as keyof Record<string, number>]: 0 });\n if (socialTitle) gsap.set(socialTitle, { opacity: 0 });\n if (socialLinks.length) gsap.set(socialLinks, { y: 25, opacity: 0 });\n\n const tl = gsap.timeline({ paused: true });\n\n layerStates.forEach((ls: { el: HTMLElement; start: number }, i: number) => {\n tl.fromTo(ls.el, { xPercent: ls.start }, { xPercent: 0, duration: 0.5, ease: 'power4.out' }, i * 0.07);\n });\n\n const lastTime = layerStates.length ? (layerStates.length - 1) * 0.07 : 0;\n const panelInsertTime = lastTime + (layerStates.length ? 0.08 : 0);\n const panelDuration = 0.65;\n\n tl.fromTo(\n panel,\n { xPercent: panelStart },\n { xPercent: 0, duration: panelDuration, ease: 'power4.out' },\n panelInsertTime\n );\n\n if (itemEls.length) {\n const itemsStartRatio = 0.15;\n const itemsStart = panelInsertTime + panelDuration * itemsStartRatio;\n\n tl.to(\n itemEls,\n { yPercent: 0, rotate: 0, duration: 1, ease: 'power4.out', stagger: { each: 0.1, from: 'start' } },\n itemsStart\n );\n\n if (numberEls.length) {\n tl.to(\n numberEls,\n {\n duration: 0.6,\n ease: 'power2.out',\n ['--sm-num-opacity' as keyof Record<string, number>]: 1,\n stagger: { each: 0.08, from: 'start' }\n },\n itemsStart + 0.1\n );\n }\n }\n\n if (socialTitle || socialLinks.length) {\n const socialsStart = panelInsertTime + panelDuration * 0.4;\n\n if (socialTitle) tl.to(socialTitle, { opacity: 1, duration: 0.5, ease: 'power2.out' }, socialsStart);\n if (socialLinks.length) {\n tl.to(\n socialLinks,\n {\n y: 0,\n opacity: 1,\n duration: 0.55,\n ease: 'power3.out',\n stagger: { each: 0.08, from: 'start' },\n onComplete: () => {\n gsap.set(socialLinks, { clearProps: 'opacity' });\n }\n },\n socialsStart + 0.04\n );\n }\n }\n\n openTlRef.value = tl;\n return tl;\n};\n\nconst playOpen = () => {\n if (busyRef.value) return;\n busyRef.value = true;\n const tl = buildOpenTimeline();\n if (tl) {\n tl.eventCallback('onComplete', () => {\n busyRef.value = false;\n });\n tl.play(0);\n } else {\n busyRef.value = false;\n }\n};\n\nconst playClose = () => {\n openTlRef.value?.kill();\n openTlRef.value = null;\n itemEntranceTweenRef.value?.kill();\n\n const panel = panelRef.value;\n const layers = preLayerElsRef.value;\n if (!panel) return;\n\n const all: HTMLElement[] = [...layers, panel];\n closeTweenRef.value?.kill();\n\n const offscreen = props.position === 'left' ? -100 : 100;\n\n closeTweenRef.value = gsap.to(all, {\n xPercent: offscreen,\n duration: 0.32,\n ease: 'power3.in',\n overwrite: 'auto',\n onComplete: () => {\n const itemEls = Array.from(panel.querySelectorAll('.sm-panel-itemLabel')) as HTMLElement[];\n if (itemEls.length) gsap.set(itemEls, { yPercent: 140, rotate: 10 });\n\n const numberEls = Array.from(\n panel.querySelectorAll('.sm-panel-list[data-numbering] .sm-panel-item')\n ) as HTMLElement[];\n if (numberEls.length) gsap.set(numberEls, { ['--sm-num-opacity' as keyof Record<string, number>]: 0 });\n\n const socialTitle = panel.querySelector('.sm-socials-title') as HTMLElement | null;\n const socialLinks = Array.from(panel.querySelectorAll('.sm-socials-link')) as HTMLElement[];\n if (socialTitle) gsap.set(socialTitle, { opacity: 0 });\n if (socialLinks.length) gsap.set(socialLinks, { y: 25, opacity: 0 });\n\n busyRef.value = false;\n }\n });\n};\n\nconst animateIcon = (opening: boolean) => {\n const icon = iconRef.value;\n const h = plusHRef.value;\n const v = plusVRef.value;\n if (!icon || !h || !v) return;\n\n spinTweenRef.value?.kill();\n\n if (opening) {\n gsap.set(icon, { rotate: 0, transformOrigin: '50% 50%' });\n spinTweenRef.value = gsap\n .timeline({ defaults: { ease: 'power4.out' } })\n .to(h, { rotate: 45, duration: 0.5 }, 0)\n .to(v, { rotate: -45, duration: 0.5 }, 0);\n } else {\n spinTweenRef.value = gsap\n .timeline({ defaults: { ease: 'power3.inOut' } })\n .to(h, { rotate: 0, duration: 0.35 }, 0)\n .to(v, { rotate: 90, duration: 0.35 }, 0)\n .to(icon, { rotate: 0, duration: 0.001 }, 0);\n }\n};\n\nconst animateColor = (opening: boolean) => {\n const btn = toggleBtnRef.value;\n if (!btn) return;\n colorTweenRef.value?.kill();\n if (props.changeMenuColorOnOpen) {\n const targetColor = opening ? props.openMenuButtonColor : props.menuButtonColor;\n colorTweenRef.value = gsap.to(btn, { color: targetColor, delay: 0.18, duration: 0.3, ease: 'power2.out' });\n } else {\n gsap.set(btn, { color: props.menuButtonColor });\n }\n};\n\nconst animateText = (opening: boolean) => {\n const inner = textInnerRef.value;\n if (!inner) return;\n\n textCycleAnimRef.value?.kill();\n\n const valueLabel = opening ? 'Menu' : 'Close';\n const targetLabel = opening ? 'Close' : 'Menu';\n const cycles = 3;\n\n const seq: string[] = [valueLabel];\n let last = valueLabel;\n for (let i = 0; i < cycles; i++) {\n last = last === 'Menu' ? 'Close' : 'Menu';\n seq.push(last);\n }\n if (last !== targetLabel) seq.push(targetLabel);\n seq.push(targetLabel);\n\n textLines.value = seq;\n gsap.set(inner, { yPercent: 0 });\n\n const lineCount = seq.length;\n const finalShift = ((lineCount - 1) / lineCount) * 100;\n\n textCycleAnimRef.value = gsap.to(inner, {\n yPercent: -finalShift,\n duration: 0.5 + lineCount * 0.07,\n ease: 'power4.out'\n });\n};\n\nconst toggleMenu = () => {\n const target = !openRef.value;\n openRef.value = target;\n open.value = target;\n\n if (target) {\n props.onMenuOpen?.();\n playOpen();\n } else {\n props.onMenuClose?.();\n playClose();\n }\n\n animateIcon(target);\n animateColor(target);\n animateText(target);\n};\n\nwatch(\n () => [props.changeMenuColorOnOpen, props.menuButtonColor, props.openMenuButtonColor],\n () => {\n if (toggleBtnRef.value) {\n if (props.changeMenuColorOnOpen) {\n const targetColor = openRef.value ? props.openMenuButtonColor : props.menuButtonColor;\n gsap.set(toggleBtnRef.value, { color: targetColor });\n } else {\n gsap.set(toggleBtnRef.value, { color: props.menuButtonColor });\n }\n }\n }\n);\n\nwatch(\n () => [props.menuButtonColor, props.position],\n () => {\n nextTick(() => {\n if (gsapContext) {\n gsapContext.revert();\n }\n initializeGSAP();\n });\n }\n);\n\nonMounted(() => {\n nextTick(() => {\n initializeGSAP();\n });\n});\n\nonBeforeUnmount(() => {\n openTlRef.value?.kill();\n closeTweenRef.value?.kill();\n spinTweenRef.value?.kill();\n textCycleAnimRef.value?.kill();\n colorTweenRef.value?.kill();\n itemEntranceTweenRef.value?.kill();\n\n if (gsapContext) {\n gsapContext.revert();\n }\n});\n</script>\n\n<style scoped>\n.sm-scope .staggered-menu-wrapper {\n position: relative;\n width: 100%;\n height: 100%;\n z-index: 40;\n}\n\n.sm-scope .staggered-menu-header {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2em;\n background: transparent;\n pointer-events: none;\n z-index: 20;\n}\n\n.sm-scope .staggered-menu-header > * {\n pointer-events: auto;\n}\n\n.sm-scope .sm-logo {\n display: flex;\n align-items: center;\n user-select: none;\n}\n\n.sm-scope .sm-logo-img {\n display: block;\n height: 32px;\n width: auto;\n object-fit: contain;\n}\n\n.sm-scope .sm-toggle {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n background: transparent;\n border: none;\n cursor: pointer;\n color: #e9e9ef;\n font-weight: 500;\n line-height: 1;\n overflow: visible;\n}\n\n.sm-scope .sm-toggle:focus-visible {\n outline: 2px solid #ffffffaa;\n outline-offset: 4px;\n border-radius: 4px;\n}\n\n.sm-scope .sm-line:last-of-type {\n margin-top: 6px;\n}\n\n.sm-scope .sm-toggle-textWrap {\n position: relative;\n margin-right: 0.5em;\n display: inline-block;\n height: 1em;\n overflow: hidden;\n white-space: nowrap;\n width: var(--sm-toggle-width, auto);\n min-width: var(--sm-toggle-width, auto);\n}\n\n.sm-scope .sm-toggle-textInner {\n display: flex;\n flex-direction: column;\n line-height: 1;\n}\n\n.sm-scope .sm-toggle-line {\n display: block;\n height: 1em;\n line-height: 1;\n}\n\n.sm-scope .sm-icon {\n position: relative;\n width: 14px;\n height: 14px;\n flex: 0 0 14px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n will-change: transform;\n}\n\n.sm-scope .sm-panel-itemWrap {\n position: relative;\n overflow: hidden;\n line-height: 1;\n}\n\n.sm-scope .sm-icon-line {\n position: absolute;\n left: 50%;\n top: 50%;\n width: 100%;\n height: 2px;\n background: currentColor;\n border-radius: 2px;\n transform: translate(-50%, -50%);\n will-change: transform;\n}\n\n.sm-scope .sm-line {\n display: none !important;\n}\n\n.sm-scope .staggered-menu-panel {\n position: absolute;\n top: 0;\n right: 0;\n width: clamp(260px, 38vw, 420px);\n height: 100%;\n background: white;\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n display: flex;\n flex-direction: column;\n padding: 6em 2em 2em 2em;\n overflow-y: auto;\n z-index: 10;\n}\n\n.sm-scope [data-position='left'] .staggered-menu-panel {\n right: auto;\n left: 0;\n}\n\n.sm-scope .sm-prelayers {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: clamp(260px, 38vw, 420px);\n pointer-events: none;\n z-index: 5;\n}\n\n.sm-scope [data-position='left'] .sm-prelayers {\n right: auto;\n left: 0;\n}\n\n.sm-scope .sm-prelayer {\n position: absolute;\n top: 0;\n right: 0;\n height: 100%;\n width: 100%;\n transform: translateX(0);\n}\n\n.sm-scope .sm-panel-inner {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n}\n\n.sm-scope .sm-socials {\n margin-top: auto;\n padding-top: 2rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.sm-scope .sm-socials-title {\n margin: 0;\n font-size: 1rem;\n font-weight: 500;\n color: var(--sm-accent, #ff0000);\n}\n\n.sm-scope .sm-socials-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 1rem;\n flex-wrap: wrap;\n}\n\n.sm-scope .sm-socials-list .sm-socials-link {\n opacity: 1;\n transition: opacity 0.3s ease;\n}\n\n.sm-scope .sm-socials-list:hover .sm-socials-link:not(:hover) {\n opacity: 0.35;\n}\n\n.sm-scope .sm-socials-list:focus-within .sm-socials-link:not(:focus-visible) {\n opacity: 0.35;\n}\n\n.sm-scope .sm-socials-list .sm-socials-link:hover,\n.sm-scope .sm-socials-list .sm-socials-link:focus-visible {\n opacity: 1;\n}\n\n.sm-scope .sm-socials-link:focus-visible {\n outline: 2px solid var(--sm-accent, #ff0000);\n outline-offset: 3px;\n}\n\n.sm-scope .sm-socials-link {\n font-size: 1.2rem;\n font-weight: 500;\n color: #111;\n text-decoration: none;\n position: relative;\n padding: 2px 0;\n display: inline-block;\n transition:\n color 0.3s ease,\n opacity 0.3s ease;\n}\n\n.sm-scope .sm-socials-link:hover {\n color: var(--sm-accent, #ff0000);\n}\n\n.sm-scope .sm-panel-title {\n margin: 0;\n font-size: 1rem;\n font-weight: 600;\n color: #fff;\n text-transform: uppercase;\n}\n\n.sm-scope .sm-panel-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.sm-scope .sm-panel-item {\n position: relative;\n color: #000;\n font-weight: 600;\n font-size: 4rem;\n cursor: pointer;\n line-height: 1;\n letter-spacing: -2px;\n text-transform: uppercase;\n transition:\n background 0.25s,\n color 0.25s;\n display: inline-block;\n text-decoration: none;\n padding-right: 1.4em;\n}\n\n.sm-scope .sm-panel-itemLabel {\n display: inline-block;\n will-change: transform;\n transform-origin: 50% 100%;\n}\n\n.sm-scope .sm-panel-item:hover {\n color: var(--sm-accent, #ff0000);\n}\n\n.sm-scope .sm-panel-list[data-numbering] {\n counter-reset: smItem;\n}\n\n.sm-scope .sm-panel-list[data-numbering] .sm-panel-item::after {\n counter-increment: smItem;\n content: counter(smItem, decimal-leading-zero);\n position: absolute;\n top: 0.1em;\n right: 3.2em;\n font-size: 18px;\n font-weight: 400;\n color: var(--sm-accent, #ff0000);\n letter-spacing: 0;\n pointer-events: none;\n user-select: none;\n opacity: var(--sm-num-opacity, 0);\n}\n\n@media (max-width: 1024px) {\n .sm-scope .staggered-menu-panel {\n width: 100%;\n left: 0;\n right: 0;\n }\n .sm-scope .staggered-menu-wrapper[data-open] .sm-logo-img {\n filter: invert(100%);\n }\n}\n\n@media (max-width: 640px) {\n .sm-scope .staggered-menu-panel {\n width: 100%;\n left: 0;\n right: 0;\n }\n .sm-scope .staggered-menu-wrapper[data-open] .sm-logo-img {\n filter: invert(100%);\n }\n}\n</style>\n","path":"StaggeredMenu/StaggeredMenu.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Components"]} |