{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-text-effect",
  "title": "Text effect",
  "description": "A component that applies animated effects to text content.",
  "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/text-effect.tsx",
      "content": "'use client';\nimport { cn } from '@/lib/utils';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { AnimatePresence, motion, MotionConfig } from 'motion/react';\nimport type {\n  TargetAndTransition,\n  Transition,\n  Variant,\n  Variants,\n} from 'motion/react';\nimport { motionElementOf } from './motion-elements';\nimport React from 'react';\n\nexport type PresetType = 'blur' | 'fade-in-blur' | 'scale' | 'fade' | 'slide';\n\nexport type PerType = 'word' | 'char' | 'line';\n\nexport type TextEffectProps = {\n  children: string;\n  per?: PerType;\n  as?: keyof React.JSX.IntrinsicElements;\n  variants?: {\n    container?: Variants;\n    item?: Variants;\n  };\n  className?: string;\n  preset?: PresetType;\n  delay?: number;\n  speedReveal?: number;\n  speedSegment?: number;\n  trigger?: boolean;\n  onAnimationComplete?: () => void;\n  onAnimationStart?: () => void;\n  segmentWrapperClassName?: string;\n  containerTransition?: Transition;\n  segmentTransition?: Transition;\n  style?: React.CSSProperties;\n};\n\nconst defaultStaggerTimes: Record<PerType, number> = {\n  char: 0.03,\n  word: 0.05,\n  line: 0.1,\n};\n\nconst defaultContainerVariants: Variants = {\n  hidden: { opacity: 0 },\n  visible: {\n    opacity: 1,\n    transition: {\n      staggerChildren: 0.05,\n    },\n  },\n  exit: {\n    transition: { staggerChildren: 0.05, staggerDirection: -1 },\n  },\n};\n\nconst defaultItemVariants: Variants = {\n  hidden: { opacity: 0 },\n  visible: {\n    opacity: 1,\n  },\n  exit: { opacity: 0 },\n};\n\nconst presetVariants: Record<\n  PresetType,\n  { container: Variants; item: Variants }\n> = {\n  blur: {\n    container: defaultContainerVariants,\n    item: {\n      hidden: { opacity: 0, filter: 'blur(12px)' },\n      visible: { opacity: 1, filter: 'blur(0px)' },\n      exit: { opacity: 0, filter: 'blur(12px)' },\n    },\n  },\n  'fade-in-blur': {\n    container: defaultContainerVariants,\n    item: {\n      hidden: { opacity: 0, y: 20, filter: 'blur(12px)' },\n      visible: { opacity: 1, y: 0, filter: 'blur(0px)' },\n      exit: { opacity: 0, y: 20, filter: 'blur(12px)' },\n    },\n  },\n  scale: {\n    container: defaultContainerVariants,\n    item: {\n      hidden: { opacity: 0, scale: 0 },\n      visible: { opacity: 1, scale: 1 },\n      exit: { opacity: 0, scale: 0 },\n    },\n  },\n  fade: {\n    container: defaultContainerVariants,\n    item: {\n      hidden: { opacity: 0 },\n      visible: { opacity: 1 },\n      exit: { opacity: 0 },\n    },\n  },\n  slide: {\n    container: defaultContainerVariants,\n    item: {\n      hidden: { opacity: 0, y: 20 },\n      visible: { opacity: 1, y: 0 },\n      exit: { opacity: 0, y: 20 },\n    },\n  },\n};\n\nconst AnimationComponent: React.FC<{\n  segment: string;\n  variants: Variants;\n  per: 'line' | 'word' | 'char';\n  segmentWrapperClassName?: string;\n}> = React.memo(({ segment, variants, per, segmentWrapperClassName }) => {\n  const content =\n    per === 'line' ? (\n      <motion.span variants={variants} className='block'>\n        {segment}\n      </motion.span>\n    ) : per === 'word' ? (\n      <motion.span\n        aria-hidden='true'\n        variants={variants}\n        className='inline-block whitespace-pre'\n      >\n        {segment}\n      </motion.span>\n    ) : (\n      <motion.span className='inline-block whitespace-pre'>\n        {segment.split('').map((char, charIndex) => (\n          <motion.span\n            key={`char-${charIndex}`}\n            aria-hidden='true'\n            variants={variants}\n            className='inline-block whitespace-pre'\n          >\n            {char}\n          </motion.span>\n        ))}\n      </motion.span>\n    );\n\n  if (!segmentWrapperClassName) {\n    return content;\n  }\n\n  const defaultWrapperClassName = per === 'line' ? 'block' : 'inline-block';\n\n  return (\n    <span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>\n      {content}\n    </span>\n  );\n});\n\nAnimationComponent.displayName = 'AnimationComponent';\n\nconst splitText = (text: string, per: PerType) => {\n  if (per === 'line') return text.split('\\n');\n  return text.split(/(\\s+)/);\n};\n\nconst hasTransition = (\n  variant?: Variant\n): variant is TargetAndTransition & { transition?: Transition } => {\n  if (!variant) return false;\n  return typeof variant === 'object' && 'transition' in variant;\n};\n\nfunction withoutExitTransition(transition: Transition & { exit?: Transition }) {\n  const mainTransition: Transition & { exit?: Transition } = { ...transition };\n  delete mainTransition.exit;\n  return mainTransition;\n}\n\nfunction ReducedMotionConfig({ children }: { children: React.ReactNode }) {\n  const prefersReducedMotion = usePrefersReducedMotion();\n  if (!prefersReducedMotion) {\n    return children;\n  }\n  return <MotionConfig reducedMotion='always'>{children}</MotionConfig>;\n}\n\nconst createVariantsWithTransition = (\n  baseVariants: Variants,\n  transition?: Transition & { exit?: Transition }\n): Variants => {\n  if (!transition) return baseVariants;\n\n  const mainTransition = withoutExitTransition(transition);\n\n  return {\n    ...baseVariants,\n    visible: {\n      ...baseVariants.visible,\n      transition: {\n        ...(hasTransition(baseVariants.visible)\n          ? baseVariants.visible.transition\n          : {}),\n        ...mainTransition,\n      },\n    },\n    exit: {\n      ...baseVariants.exit,\n      transition: {\n        ...(hasTransition(baseVariants.exit)\n          ? baseVariants.exit.transition\n          : {}),\n        ...mainTransition,\n        staggerDirection: -1,\n      },\n    },\n  };\n};\n\nexport function TextEffect({\n  children,\n  per = 'word',\n  as = 'p',\n  variants,\n  className,\n  preset = 'fade',\n  delay = 0,\n  speedReveal = 1,\n  speedSegment = 1,\n  trigger = true,\n  onAnimationComplete,\n  onAnimationStart,\n  segmentWrapperClassName,\n  containerTransition,\n  segmentTransition,\n  style,\n}: TextEffectProps) {\n  const segments = splitText(children, per);\n  const MotionTag = motionElementOf(as) as typeof motion.div;\n\n  const baseVariants = preset\n    ? presetVariants[preset]\n    : { container: defaultContainerVariants, item: defaultItemVariants };\n\n  const stagger = defaultStaggerTimes[per] / speedReveal;\n\n  const baseDuration = 0.3 / speedSegment;\n\n  const customStagger = hasTransition(variants?.container?.visible ?? {})\n    ? (variants?.container?.visible as TargetAndTransition).transition\n        ?.staggerChildren\n    : undefined;\n\n  const customDelay = hasTransition(variants?.container?.visible ?? {})\n    ? (variants?.container?.visible as TargetAndTransition).transition\n        ?.delayChildren\n    : undefined;\n\n  const computedVariants = {\n    container: createVariantsWithTransition(\n      variants?.container || baseVariants.container,\n      {\n        staggerChildren: customStagger ?? stagger,\n        delayChildren: customDelay ?? delay,\n        ...containerTransition,\n        exit: {\n          staggerChildren: customStagger ?? stagger,\n          staggerDirection: -1,\n        },\n      }\n    ),\n    item: createVariantsWithTransition(variants?.item || baseVariants.item, {\n      duration: baseDuration,\n      ...segmentTransition,\n    }),\n  };\n\n  return (\n    <ReducedMotionConfig>\n      <AnimatePresence mode='popLayout'>\n        {trigger && (\n          <MotionTag\n            initial='hidden'\n            animate='visible'\n            exit='exit'\n            variants={computedVariants.container}\n            className={className}\n            onAnimationComplete={onAnimationComplete}\n            onAnimationStart={onAnimationStart}\n            style={style}\n          >\n            {per !== 'line' ? (\n              <span className='sr-only'>{children}</span>\n            ) : null}\n            {segments.map((segment, index) => (\n              <AnimationComponent\n                key={`${per}-${index}-${segment}`}\n                segment={segment}\n                variants={computedVariants.item}\n                per={per}\n                segmentWrapperClassName={segmentWrapperClassName}\n              />\n            ))}\n          </MotionTag>\n        )}\n      </AnimatePresence>\n    </ReducedMotionConfig>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/core/text-effect.tsx"
    },
    {
      "path": "components/core/motion-elements.ts",
      "content": "import { motion } from 'motion/react';\n\nconst MOTION_ELEMENTS = {\n  a: motion.a,\n  article: motion.article,\n  aside: motion.aside,\n  blockquote: motion.blockquote,\n  button: motion.button,\n  code: motion.code,\n  div: motion.div,\n  em: motion.em,\n  figure: motion.figure,\n  footer: motion.footer,\n  h1: motion.h1,\n  h2: motion.h2,\n  h3: motion.h3,\n  h4: motion.h4,\n  h5: motion.h5,\n  h6: motion.h6,\n  header: motion.header,\n  label: motion.label,\n  li: motion.li,\n  main: motion.main,\n  nav: motion.nav,\n  ol: motion.ol,\n  p: motion.p,\n  pre: motion.pre,\n  section: motion.section,\n  small: motion.small,\n  span: motion.span,\n  strong: motion.strong,\n  ul: motion.ul,\n};\n\ntype MotionElementTag = keyof typeof MOTION_ELEMENTS;\n\nfunction isMotionElementTag(tag: unknown): tag is MotionElementTag {\n  const isString = typeof tag === 'string';\n  return isString && tag in MOTION_ELEMENTS;\n}\n\n/**\n * The motion version of an HTML tag. A lookup such as `motion[tag]` is invisible to the bundler,\n * which drops the tags it never sees and breaks the production build, so every tag is listed here.\n */\nexport function motionElementOf(tag: unknown) {\n  if (isMotionElementTag(tag)) {\n    return MOTION_ELEMENTS[tag];\n  }\n  return MOTION_ELEMENTS.div;\n}\n",
      "type": "registry:component",
      "target": "components/core/motion-elements.ts"
    }
  ],
  "categories": [
    "motion"
  ],
  "type": "registry:component"
}