{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-inertia",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "text-inertia.tsx",
      "content": "\"use client\";\n\nimport { motion, useMotionValue, useSpring } from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type PointerEvent as ReactPointerEvent,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type TextInertiaProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  \"children\"\n> & {\n  text?: string;\n  intensity?: number;\n  wordClassName?: string;\n};\n\ntype PointerVelocity = {\n  x: number;\n  y: number;\n};\n\nconst DEFAULT_TEXT = \"Interfaces remember momentum\";\nconst MAX_TRANSLATE = 46;\nconst MAX_ROTATION = 18;\nconst WORD_SPLIT_PATTERN = /\\s+/;\nconst RESET_DELAY = 150;\nconst INERTIA_SPRING = {\n  stiffness: 58,\n  damping: 16,\n  mass: 1.35,\n  restDelta: 0.001,\n} as const;\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction withFallback(value: number, fallback: number) {\n  return Math.abs(value) < 1 ? fallback : value;\n}\n\nfunction InertiaWord({\n  children,\n  index,\n  intensity,\n  velocityRef,\n  wordClassName,\n}: {\n  children: string;\n  index: number;\n  intensity: number;\n  velocityRef: RefObject<PointerVelocity>;\n  wordClassName?: string;\n}) {\n  const xTarget = useMotionValue(0);\n  const yTarget = useMotionValue(0);\n  const rotateTarget = useMotionValue(0);\n  const x = useSpring(xTarget, INERTIA_SPRING);\n  const y = useSpring(yTarget, INERTIA_SPRING);\n  const rotate = useSpring(rotateTarget, INERTIA_SPRING);\n  const resetTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n    undefined\n  );\n\n  const clearResetTimer = useCallback(() => {\n    if (resetTimerRef.current) {\n      clearTimeout(resetTimerRef.current);\n    }\n    resetTimerRef.current = undefined;\n  }, []);\n\n  const resetTargets = useCallback(() => {\n    xTarget.set(0);\n    yTarget.set(0);\n    rotateTarget.set(0);\n  }, [rotateTarget, xTarget, yTarget]);\n\n  useEffect(\n    () => () => {\n      clearResetTimer();\n    },\n    [clearResetTimer]\n  );\n\n  const handlePointerEnter = () => {\n    const direction = index % 2 === 0 ? 1 : -1;\n    const velocity = velocityRef.current;\n    const xKick = clamp(\n      withFallback(velocity.x * intensity * 2.2, direction * intensity * 10),\n      -MAX_TRANSLATE,\n      MAX_TRANSLATE\n    );\n    const yKick = clamp(\n      withFallback(velocity.y * intensity * 2.2, -intensity * 7),\n      -MAX_TRANSLATE,\n      MAX_TRANSLATE\n    );\n    const rotateKick = clamp(\n      withFallback(\n        (velocity.x - velocity.y) * intensity * 0.5,\n        direction * intensity * 6\n      ),\n      -MAX_ROTATION,\n      MAX_ROTATION\n    );\n\n    clearResetTimer();\n    xTarget.set(xKick);\n    yTarget.set(yKick);\n    rotateTarget.set(rotateKick);\n    resetTimerRef.current = setTimeout(() => {\n      resetTargets();\n      resetTimerRef.current = undefined;\n    }, RESET_DELAY);\n  };\n\n  return (\n    <motion.span\n      aria-hidden=\"true\"\n      className={cn(\n        \"inline-block cursor-default select-none will-change-transform\",\n        wordClassName\n      )}\n      onPointerEnter={handlePointerEnter}\n      style={{ rotate, x, y }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nexport default function TextInertia({\n  text = DEFAULT_TEXT,\n  className,\n  wordClassName,\n  intensity = 1,\n  \"aria-label\": ariaLabel,\n  onPointerLeave,\n  onPointerMove,\n  ...props\n}: TextInertiaProps) {\n  const velocityRef = useRef<PointerVelocity>({ x: 0, y: 0 });\n  const lastPointRef = useRef<{ x: number; y: number } | null>(null);\n  const words = useMemo(\n    () => text.trim().split(WORD_SPLIT_PATTERN).filter(Boolean),\n    [text]\n  );\n\n  const handlePointerMove = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      const lastPoint = lastPointRef.current;\n\n      if (lastPoint) {\n        velocityRef.current = {\n          x: event.clientX - lastPoint.x,\n          y: event.clientY - lastPoint.y,\n        };\n      }\n\n      lastPointRef.current = { x: event.clientX, y: event.clientY };\n      onPointerMove?.(event);\n    },\n    [onPointerMove]\n  );\n\n  const handlePointerLeave = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      lastPointRef.current = null;\n      velocityRef.current = { x: 0, y: 0 };\n      onPointerLeave?.(event);\n    },\n    [onPointerLeave]\n  );\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-wrap items-center justify-center gap-x-[0.3em] gap-y-[0.12em] text-center leading-none\",\n        className\n      )}\n      onPointerLeave={handlePointerLeave}\n      onPointerMove={handlePointerMove}\n      {...props}\n    >\n      <span className=\"sr-only\">{ariaLabel ?? text}</span>\n      {words.map((word, index) => (\n        <InertiaWord\n          index={index}\n          intensity={intensity}\n          key={`${word}-${index}`}\n          velocityRef={velocityRef}\n          wordClassName={wordClassName}\n        >\n          {word}\n        </InertiaWord>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "Text Inertia",
  "description": "Pointer-reactive word treatment that maps cursor velocity into Motion-powered spring movement."
}
