import type { Directive, DirectiveBinding } from 'vue'
/**
* v-reveal — animate an element into view on first scroll-intersection.
*
*
…
// default: fade + rise
* …
// slide from a direction
* …
* // stagger
*
* Adds `.bgl-reveal` immediately (hidden, pre-transformed) and toggles
* `.bgl-reveal-in` once the element enters the viewport. Honors
* `prefers-reduced-motion` (shows instantly, no transform). All visual tuning
* lives in CSS (motion.css) via data-attributes, so it's themeable and cheap.
*/
type Dir = 'up' | 'down' | 'left' | 'right' | 'none'
interface RevealOptions {
/** Direction the element travels in from. Default 'up'. */
y?: Dir
/** Stagger / entrance delay in ms. */
delay?: number
/** Animate only once (default) or re-run when scrolled away and back. */
once?: boolean
/** 0–1 visibility threshold before triggering. Default 0.12. */
threshold?: number
}
const prefersReducedMotion = () =>
typeof window !== 'undefined'
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
function parse(value: RevealOptions | Dir | undefined): Required {
const base: Required = { y: 'up', delay: 0, once: true, threshold: 0.12 }
if (!value) { return base }
if (typeof value === 'string') { return { ...base, y: value } }
return { ...base, ...value }
}
const observers = new WeakMap()
const reveal: Directive = {
mounted(el, binding: DirectiveBinding) {
const opts = parse(binding.value)
// Reduced motion: reveal instantly, skip all transforms/observers.
if (prefersReducedMotion()) { return }
el.classList.add('bgl-reveal')
if (opts.y !== 'none') { el.dataset.revealDir = opts.y }
if (opts.delay) { el.style.setProperty('--bgl-reveal-delay', `${opts.delay}ms`) }
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
el.classList.add('bgl-reveal-in')
if (opts.once) { observer.unobserve(el) }
} else if (!opts.once) {
el.classList.remove('bgl-reveal-in')
}
}
}, { threshold: opts.threshold, rootMargin: '0px 0px -8% 0px' })
observer.observe(el)
observers.set(el, observer)
},
unmounted(el) {
observers.get(el)?.disconnect()
observers.delete(el)
},
getSSRProps() {
return {}
},
}
export default reveal
export type { RevealOptions }