{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-scroll-animation",
  "title": "Scroll animation",
  "description": "Reveals that play once when an element scrolls into view, and a parallax layer that drifts with the scroll.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "components/core/scroll-animation.tsx",
      "content": "'use client';\nimport {\n  motion,\n  type MotionValue,\n  type Transition,\n  useInView,\n  useScroll,\n  useTransform,\n  type Variants,\n} from 'motion/react';\nimport { type ReactNode, type RefObject, useRef } from 'react';\nimport { cn } from '@/lib/utils';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\n\nconst EASE_OUT_STRONG = [0.23, 1, 0.32, 1] as const;\n\nexport type ScrollAnimationPreset = 'fade-up' | 'scale-in' | 'clip-reveal';\n\ntype Preset = {\n  variants: Variants;\n  duration: number;\n};\n\nconst PRESETS: Record<ScrollAnimationPreset, Preset> = {\n  'fade-up': {\n    variants: {\n      hidden: { opacity: 0, y: 16 },\n      visible: { opacity: 1, y: 0 },\n    },\n    duration: 0.5,\n  },\n  'scale-in': {\n    variants: {\n      hidden: { opacity: 0, scale: 0.96 },\n      visible: { opacity: 1, scale: 1 },\n    },\n    duration: 0.5,\n  },\n  'clip-reveal': {\n    variants: {\n      hidden: { clipPath: 'inset(100% 0% 0% 0%)' },\n      visible: { clipPath: 'inset(0% 0% 0% 0%)' },\n    },\n    duration: 0.7,\n  },\n};\n\nfunction revealStateOf(isRevealed: boolean) {\n  if (isRevealed) {\n    return 'visible';\n  }\n  return 'hidden';\n}\n\n// Under reduced motion both states are the final one, so the element is written in place on\n// mount instead of keeping the hidden styles rendered on the server.\nfunction variantsOf(\n  preset: ScrollAnimationPreset,\n  prefersReducedMotion: boolean | null\n): Variants {\n  const { variants } = PRESETS[preset];\n  if (prefersReducedMotion) {\n    return { hidden: variants.visible, visible: variants.visible };\n  }\n  return variants;\n}\n\nexport type ScrollAnimationProps = {\n  children: ReactNode;\n  preset?: ScrollAnimationPreset;\n  delay?: number;\n  amount?: number;\n  className?: string;\n  containerRef?: RefObject<HTMLElement | null>;\n};\n\nexport function ScrollAnimation({\n  children,\n  preset = 'fade-up',\n  delay = 0,\n  amount = 0.3,\n  className,\n  containerRef,\n}: ScrollAnimationProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const prefersReducedMotion = usePrefersReducedMotion();\n  const isInView = useInView(ref, { root: containerRef, once: true, amount });\n  const revealState = revealStateOf(isInView);\n  const variants = variantsOf(preset, prefersReducedMotion);\n  const transition: Transition = {\n    duration: PRESETS[preset].duration,\n    ease: EASE_OUT_STRONG,\n    delay,\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      initial='hidden'\n      animate={revealState}\n      variants={variants}\n      transition={transition}\n      className={className}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nfunction layerOffsetOf(\n  prefersReducedMotion: boolean | null,\n  offset: MotionValue<number>\n) {\n  if (prefersReducedMotion) {\n    return 0;\n  }\n  return offset;\n}\n\nexport type ScrollParallaxProps = {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n  layerClassName?: string;\n  containerRef?: RefObject<HTMLElement | null>;\n};\n\nexport function ScrollParallax({\n  children,\n  strength = 40,\n  className,\n  layerClassName,\n  containerRef,\n}: ScrollParallaxProps) {\n  const frameRef = useRef<HTMLDivElement>(null);\n  const prefersReducedMotion = usePrefersReducedMotion();\n  const { scrollYProgress } = useScroll({\n    target: frameRef,\n    container: containerRef,\n    offset: ['start end', 'end start'],\n  });\n  const offset = useTransform(scrollYProgress, [0, 1], [-strength, strength]);\n  const reach = Math.abs(strength);\n  const bleed = -reach;\n  const layerOffset = layerOffsetOf(prefersReducedMotion, offset);\n\n  return (\n    <div ref={frameRef} className={cn('relative overflow-clip', className)}>\n      <motion.div\n        style={{\n          y: layerOffset,\n          top: bleed,\n          bottom: bleed,\n        }}\n        className={cn('absolute inset-x-0', layerClassName)}\n      >\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/core/scroll-animation.tsx"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:component"
}