{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-text",
  "title": "Liquid Text",
  "description": "Text distorted through an animated SVG noise filter, 5 presets (liquid, melt, blob, ghost, crystal), cursor-proximity intensity, and a click burst ripple.",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/liquid-text/liquid-text.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport type LiquidTextPreset = \"liquid\" | \"melt\" | \"blob\" | \"ghost\" | \"crystal\";\nexport type LiquidTextGradientDirection = \"horizontal\" | \"vertical\" | \"radial\";\n\ninterface PresetDef {\n  bfx: number;\n  bfy: number;\n  octaves: number;\n  turbType: \"fractalNoise\" | \"turbulence\";\n  baseScale: number;\n  morphRadius: number;\n  blurStd: number;\n  breatheAmp: number;\n  breatheSpeed: number;\n  evolveX: number;\n  evolveY: number;\n}\n\nconst PRESETS: Record<LiquidTextPreset, PresetDef> = {\n  liquid: { bfx: 0.014, bfy: 0.011, octaves: 3, turbType: \"fractalNoise\", baseScale: 16, morphRadius: 0, blurStd: 0, breatheAmp: 8, breatheSpeed: 0.45, evolveX: 0.006, evolveY: 0.004 },\n  melt: { bfx: 0.009, bfy: 0.022, octaves: 2, turbType: \"fractalNoise\", baseScale: 28, morphRadius: 0, blurStd: 0, breatheAmp: 14, breatheSpeed: 0.25, evolveX: 0.003, evolveY: 0.009 },\n  blob: { bfx: 0.018, bfy: 0.018, octaves: 2, turbType: \"fractalNoise\", baseScale: 10, morphRadius: 5, blurStd: 1.5, breatheAmp: 5, breatheSpeed: 0.9, evolveX: 0.007, evolveY: 0.007 },\n  ghost: { bfx: 0.022, bfy: 0.022, octaves: 4, turbType: \"fractalNoise\", baseScale: 22, morphRadius: 0, blurStd: 3.5, breatheAmp: 18, breatheSpeed: 0.35, evolveX: 0.009, evolveY: 0.007 },\n  crystal: { bfx: 0.038, bfy: 0.038, octaves: 1, turbType: \"turbulence\", baseScale: 20, morphRadius: 0, blurStd: 0, breatheAmp: 12, breatheSpeed: 1.3, evolveX: 0.015, evolveY: 0.012 },\n};\n\nexport interface LiquidTextProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  text?: string;\n  fontSize?: number;\n  autoFit?: boolean;\n  fitPadding?: number;\n  fontWeight?: string;\n  fontFamily?: string;\n  letterSpacing?: number;\n  textColor?: string;\n  backgroundColor?: string;\n  useGradient?: boolean;\n  gradientColorTo?: string;\n  gradientDirection?: LiquidTextGradientDirection;\n  preset?: LiquidTextPreset;\n  scaleMultiplier?: number;\n  mouseEnabled?: boolean;\n  mouseStrength?: number;\n  mouseRadius?: number;\n  clickBurst?: boolean;\n  burstStrength?: number;\n  useGlow?: boolean;\n  glowColor?: string;\n  glowRadius?: number;\n}\n\nexport function LiquidText({\n  text = \"Liquid\",\n  fontSize = 120,\n  autoFit = false,\n  fitPadding = 5,\n  fontWeight = \"700\",\n  fontFamily = \"Inter, sans-serif\",\n  letterSpacing = -2,\n  textColor = \"#87FFE3\",\n  backgroundColor = \"#000000\",\n  useGradient = false,\n  gradientColorTo = \"#ff55ee\",\n  gradientDirection = \"horizontal\",\n  preset = \"liquid\",\n  scaleMultiplier = 1,\n  mouseEnabled = true,\n  mouseStrength = 2,\n  mouseRadius = 300,\n  clickBurst = false,\n  burstStrength = 3,\n  useGlow = false,\n  glowColor = \"#87FFE3\",\n  glowRadius = 20,\n  className,\n  style,\n  ...props\n}: LiquidTextProps) {\n  const filterId = `blob-fx-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const turbRef = React.useRef<SVGFETurbulenceElement>(null);\n  const dispRef = React.useRef<SVGFEDisplacementMapElement>(null);\n  const morphRef = React.useRef<SVGFEMorphologyElement>(null);\n  const mouseRef = React.useRef({ x: -9999, y: -9999 });\n  const currentScaleRef = React.useRef(PRESETS[preset].baseScale);\n  const burstRef = React.useRef({ active: false, startTime: 0, strength: 0 });\n  const rafRef = React.useRef(0);\n\n  const [computedFontSize, setComputedFontSize] = React.useState(fontSize);\n\n  const live = React.useRef({ preset, scaleMultiplier, mouseEnabled, mouseStrength, mouseRadius, clickBurst, burstStrength });\n  React.useEffect(() => {\n    const lc = live.current;\n    lc.preset = preset;\n    lc.scaleMultiplier = scaleMultiplier;\n    lc.mouseEnabled = mouseEnabled;\n    lc.mouseStrength = mouseStrength;\n    lc.mouseRadius = mouseRadius;\n    lc.clickBurst = clickBurst;\n    lc.burstStrength = burstStrength;\n  }, [preset, scaleMultiplier, mouseEnabled, mouseStrength, mouseRadius, clickBurst, burstStrength]);\n\n  React.useEffect(() => {\n    currentScaleRef.current = PRESETS[preset].baseScale;\n  }, [preset]);\n\n  const computeFontSizeFn = React.useCallback(() => {\n    if (!autoFit) return;\n    const container = containerRef.current;\n    if (!container) return;\n    const cw = container.offsetWidth;\n    const ch = container.offsetHeight;\n    if (!cw || !ch) return;\n\n    const off = document.createElement(\"canvas\");\n    const ctx = off.getContext(\"2d\")!;\n    ctx.font = `${fontWeight} 100px ${fontFamily}`;\n    const w100 = ctx.measureText(text).width;\n    if (w100 === 0) return;\n    const padded = cw * (1 - (fitPadding * 2) / 100);\n    setComputedFontSize(Math.min(Math.floor((padded / w100) * 100), Math.floor(ch * 0.85)));\n  }, [autoFit, fontWeight, fontFamily, text, fitPadding]);\n\n  React.useEffect(() => {\n    if (!autoFit) return;\n    const container = containerRef.current;\n    if (!container) return;\n    let timer = 0;\n    const ro = new ResizeObserver(() => {\n      clearTimeout(timer);\n      timer = window.setTimeout(computeFontSizeFn, 80);\n    });\n    ro.observe(container);\n    computeFontSizeFn();\n    return () => {\n      ro.disconnect();\n      clearTimeout(timer);\n    };\n  }, [autoFit, computeFontSizeFn]);\n\n  const resolvedFontSize = autoFit ? computedFontSize : fontSize;\n\n  React.useEffect(() => {\n    let alive = true;\n\n    const tick = (ts: number) => {\n      if (!alive) return;\n\n      const cfg = live.current;\n      const p = PRESETS[cfg.preset];\n      const t = ts * 0.001;\n\n      const bfx = p.bfx + Math.sin(t * p.evolveX * 10) * p.bfx * 0.4;\n      const bfy = p.bfy + Math.cos(t * p.evolveY * 10 * 0.77) * p.bfy * 0.35;\n\n      const breathe = Math.sin(t * p.breatheSpeed) * p.breatheAmp;\n\n      let mouseBoost = 0;\n      if (cfg.mouseEnabled) {\n        const { x: mx, y: my } = mouseRef.current;\n        if (mx > -9000 && containerRef.current) {\n          const w = containerRef.current.offsetWidth;\n          const h = containerRef.current.offsetHeight;\n          const dist = Math.sqrt((mx - w * 0.5) ** 2 + (my - h * 0.5) ** 2);\n          const proximity = Math.max(0, 1 - dist / Math.max(1, cfg.mouseRadius));\n          mouseBoost = proximity * cfg.mouseStrength * p.baseScale * 0.5;\n        }\n      }\n\n      let burstBoost = 0;\n      if (burstRef.current.active) {\n        const elapsed = t - burstRef.current.startTime;\n        const dur = 0.75;\n        if (elapsed < dur) {\n          burstBoost = Math.pow(1 - elapsed / dur, 2) * burstRef.current.strength * p.baseScale;\n        } else {\n          burstRef.current.active = false;\n        }\n      }\n\n      const targetScale = (p.baseScale + breathe + mouseBoost + burstBoost) * cfg.scaleMultiplier;\n      currentScaleRef.current += (targetScale - currentScaleRef.current) * 0.08;\n\n      turbRef.current?.setAttribute(\"baseFrequency\", `${bfx.toFixed(5)} ${bfy.toFixed(5)}`);\n      dispRef.current?.setAttribute(\"scale\", `${currentScaleRef.current.toFixed(2)}`);\n\n      if (morphRef.current) {\n        if (p.morphRadius > 0) {\n          const r = p.morphRadius + Math.sin(t * p.breatheSpeed * 1.3) * p.morphRadius * 0.3;\n          morphRef.current.setAttribute(\"radius\", r.toFixed(2));\n        } else {\n          morphRef.current.setAttribute(\"radius\", \"0\");\n        }\n      }\n\n      rafRef.current = requestAnimationFrame(tick);\n    };\n\n    rafRef.current = requestAnimationFrame(tick);\n    return () => {\n      alive = false;\n      cancelAnimationFrame(rafRef.current);\n    };\n  }, []);\n\n  const textStyle: React.CSSProperties = {\n    display: \"block\",\n    fontSize: resolvedFontSize,\n    fontWeight,\n    fontFamily,\n    letterSpacing: `${letterSpacing}px`,\n    lineHeight: 1.1,\n    textAlign: \"center\",\n    whiteSpace: \"pre-line\",\n    userSelect: \"none\",\n    pointerEvents: \"none\",\n    ...(useGradient\n      ? {\n          background:\n            gradientDirection === \"radial\"\n              ? `radial-gradient(circle, ${textColor}, ${gradientColorTo})`\n              : `linear-gradient(${gradientDirection === \"vertical\" ? \"180deg\" : \"90deg\"}, ${textColor}, ${gradientColorTo})`,\n          WebkitBackgroundClip: \"text\",\n          WebkitTextFillColor: \"transparent\",\n          backgroundClip: \"text\",\n          color: \"transparent\",\n        }\n      : { color: textColor }),\n    ...(useGlow ? { textShadow: `0 0 ${glowRadius}px ${glowColor}, 0 0 ${glowRadius * 2.2}px ${glowColor}55` } : {}),\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative flex h-full w-full items-center justify-center overflow-hidden [touch-action:none]\", className)}\n      style={{ backgroundColor, cursor: clickBurst ? \"pointer\" : \"default\", ...style }}\n      onPointerMove={(e) => {\n        const r = containerRef.current!.getBoundingClientRect();\n        mouseRef.current = { x: e.clientX - r.left, y: e.clientY - r.top };\n      }}\n      onPointerLeave={() => {\n        mouseRef.current = { x: -9999, y: -9999 };\n      }}\n      onPointerUp={(e) => {\n        if (e.pointerType === \"touch\") mouseRef.current = { x: -9999, y: -9999 };\n        if (live.current.clickBurst) {\n          burstRef.current = { active: true, startTime: performance.now() * 0.001, strength: live.current.burstStrength };\n        }\n      }}\n      {...props}\n    >\n      <svg width=\"0\" height=\"0\" className=\"absolute overflow-visible\" aria-hidden=\"true\">\n        <defs>\n          <filter id={filterId} x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\" colorInterpolationFilters=\"sRGB\">\n            <feTurbulence ref={turbRef} type={PRESETS[preset].turbType} baseFrequency={`${PRESETS[preset].bfx} ${PRESETS[preset].bfy}`} numOctaves={PRESETS[preset].octaves} seed=\"2\" result=\"noise\" />\n            <feDisplacementMap ref={dispRef} in=\"SourceGraphic\" in2=\"noise\" scale={String(PRESETS[preset].baseScale)} xChannelSelector=\"R\" yChannelSelector=\"G\" result=\"displaced\" />\n            <feMorphology ref={morphRef} operator=\"dilate\" radius={String(PRESETS[preset].morphRadius)} in=\"displaced\" result=\"morphed\" />\n            <feGaussianBlur stdDeviation={String(PRESETS[preset].blurStd)} in=\"morphed\" />\n          </filter>\n        </defs>\n      </svg>\n\n      <div className=\"box-border flex h-full w-full items-center justify-center p-[60px] [will-change:filter]\" style={{ filter: `url(#${filterId})` }}>\n        <span style={textStyle}>{text}</span>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}