{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dia-text",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "dia-text.tsx",
      "content": "\"use client\";\n\nimport {\n  type AnimationPlaybackControls,\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useTransform,\n} from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type CSSProperties,\n  forwardRef,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst DEFAULT_COLORS = [\"#c679c4\", \"#fa3d1d\", \"#ffb005\", \"#e1e1fe\", \"#0358f7\"];\nconst BAND_HALF = 17;\nconst SWEEP_START = -BAND_HALF;\nconst SWEEP_END = 100 + BAND_HALF;\nconst TEXT_SWAP_EASE = [0.22, 1, 0.36, 1] as const;\n\ntype DiaTextMotionProps = ComponentPropsWithoutRef<typeof motion.span>;\n\nexport type DiaTextRevealProps = Omit<\n  DiaTextMotionProps,\n  \"children\" | \"style\" | \"animate\" | \"transition\" | \"color\" | \"className\"\n> & {\n  text: string | string[];\n  colors?: string[];\n  textColor?: string;\n  duration?: number;\n  delay?: number;\n  repeat?: boolean;\n  repeatDelay?: number;\n  triggerOnView?: boolean;\n  once?: boolean;\n  className?: string;\n  fixedWidth?: boolean;\n};\n\nconst sweepEase = (t: number) =>\n  t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;\n\nfunction buildGradient(pos: number, colors: string[], textColor: string) {\n  const bandStart = pos - BAND_HALF;\n  const bandEnd = pos + BAND_HALF;\n\n  if (bandStart >= 100) {\n    return `linear-gradient(90deg, ${textColor}, ${textColor})`;\n  }\n\n  const count = colors.length;\n  const parts: string[] = [];\n\n  if (bandStart > 0) {\n    parts.push(`${textColor} 0%`, `${textColor} ${bandStart.toFixed(2)}%`);\n  }\n\n  colors.forEach((color, index) => {\n    const pct =\n      count === 1 ? pos : bandStart + (index / (count - 1)) * BAND_HALF * 2;\n\n    parts.push(`${color} ${pct.toFixed(2)}%`);\n  });\n\n  if (bandEnd < 100) {\n    parts.push(`transparent ${bandEnd.toFixed(2)}%`, \"transparent 100%\");\n  }\n\n  return `linear-gradient(90deg, ${parts.join(\", \")})`;\n}\n\nfunction measureWidths(element: HTMLElement, texts: string[]) {\n  const ghost = element.cloneNode() as HTMLElement;\n\n  Object.assign(ghost.style, {\n    position: \"absolute\",\n    visibility: \"hidden\",\n    pointerEvents: \"none\",\n    width: \"auto\",\n    whiteSpace: \"nowrap\",\n  });\n\n  element.parentElement?.appendChild(ghost);\n\n  const widths = texts.map((entry) => {\n    ghost.textContent = entry;\n    return ghost.getBoundingClientRect().width;\n  });\n\n  ghost.remove();\n  return widths;\n}\n\nconst DiaTextReveal = forwardRef<HTMLSpanElement, DiaTextRevealProps>(\n  (\n    {\n      text,\n      colors = DEFAULT_COLORS,\n      textColor = \"var(--ic-foreground)\",\n      duration = 1.5,\n      delay = 0,\n      repeat = false,\n      repeatDelay = 0.5,\n      triggerOnView = true,\n      once = true,\n      className,\n      fixedWidth = false,\n      ...props\n    },\n    ref\n  ) => {\n    const spanRef = useRef<HTMLSpanElement | null>(null);\n    const [activeIndex, setActiveIndex] = useState(0);\n    const [measuredWidths, setMeasuredWidths] = useState<number[]>([]);\n\n    const indexRef = useRef(0);\n    const hasPlayedRef = useRef(false);\n    const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n      undefined\n    );\n    const controlsRef = useRef<AnimationPlaybackControls | undefined>(\n      undefined\n    );\n    const previousTextKeyRef = useRef(\"\");\n\n    const sweepPos = useMotionValue(SWEEP_START);\n    const textOpacity = useMotionValue(1);\n    const textBlur = useMotionValue(0);\n    const textShift = useMotionValue(0);\n    const inView = useInView(spanRef, { once, amount: 0.1 });\n    const previousActiveIndexRef = useRef(0);\n\n    const texts = useMemo(() => (Array.isArray(text) ? text : [text]), [text]);\n    const textKey = useMemo(() => texts.join(\"\\0\"), [texts]);\n    const isMulti = texts.length > 1;\n    const isVisible = triggerOnView ? inView : true;\n\n    const backgroundImage = useTransform(sweepPos, (pos) =>\n      buildGradient(pos, colors, textColor)\n    );\n    const contentFilter = useTransform(\n      textBlur,\n      (blur) => `blur(${blur.toFixed(2)}px)`\n    );\n    const contentTransform = useTransform(\n      textShift,\n      (shift) => `translateY(${(-2 + shift).toFixed(2)}px)`\n    );\n\n    const fixedW = useMemo(\n      () =>\n        isMulti && fixedWidth && measuredWidths.length > 0\n          ? Math.max(...measuredWidths)\n          : undefined,\n      [fixedWidth, isMulti, measuredWidths]\n    );\n\n    const animatedW = useMemo(\n      () =>\n        isMulti && !fixedWidth && measuredWidths[activeIndex] != null\n          ? measuredWidths[activeIndex]\n          : undefined,\n      [activeIndex, fixedWidth, isMulti, measuredWidths]\n    );\n\n    const containerStyle = useMemo(\n      (): NonNullable<DiaTextMotionProps[\"style\"]> => ({\n        ...(isMulti && {\n          display: \"inline-block\",\n          overflow: \"hidden\",\n          whiteSpace: \"nowrap\",\n          verticalAlign: \"text-center\" as CSSProperties[\"verticalAlign\"],\n          ...(fixedW != null && { width: fixedW }),\n        }),\n      }),\n      [fixedW, isMulti]\n    );\n\n    const contentStyle = useMemo(\n      (): NonNullable<DiaTextMotionProps[\"style\"]> => ({\n        display: \"inline-block\",\n        color: \"transparent\",\n        backgroundClip: \"text\",\n        WebkitBackgroundClip: \"text\",\n        backgroundSize: \"100% 100%\",\n        backgroundImage,\n        opacity: textOpacity,\n        filter: contentFilter,\n        transform: contentTransform,\n        willChange: \"filter, opacity, transform\",\n      }),\n      [backgroundImage, contentFilter, contentTransform, textOpacity]\n    );\n\n    const clearCycle = useCallback(() => {\n      controlsRef.current?.stop();\n      controlsRef.current = undefined;\n\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n      }\n\n      timerRef.current = undefined;\n    }, []);\n\n    const playRef = useRef<() => void>(() => undefined);\n\n    playRef.current = () => {\n      clearCycle();\n      sweepPos.set(SWEEP_START);\n\n      controlsRef.current = animate(sweepPos, SWEEP_END, {\n        duration,\n        delay,\n        ease: sweepEase,\n        onComplete() {\n          if (!repeat || texts.length === 0) {\n            return;\n          }\n\n          timerRef.current = setTimeout(() => {\n            const next = (indexRef.current + 1) % texts.length;\n\n            indexRef.current = next;\n            setActiveIndex(next);\n            playRef.current();\n          }, repeatDelay * 1000);\n        },\n      });\n    };\n\n    useEffect(() => {\n      if (textKey === previousTextKeyRef.current) {\n        return;\n      }\n\n      previousTextKeyRef.current = textKey;\n      indexRef.current = 0;\n      setActiveIndex(0);\n      hasPlayedRef.current = false;\n      clearCycle();\n\n      sweepPos.set(SWEEP_START);\n\n      if (isVisible) {\n        hasPlayedRef.current = true;\n        playRef.current();\n      }\n    }, [clearCycle, isVisible, sweepPos, textKey]);\n\n    useEffect(() => {\n      const element = spanRef.current;\n\n      if (!(element && isMulti)) {\n        setMeasuredWidths([]);\n        return;\n      }\n\n      setMeasuredWidths(measureWidths(element, texts));\n    }, [isMulti, texts]);\n\n    useEffect(() => {\n      if (!isVisible) {\n        if (!once) {\n          hasPlayedRef.current = false;\n        }\n        return;\n      }\n\n      if (once && hasPlayedRef.current) {\n        return;\n      }\n\n      hasPlayedRef.current = true;\n      playRef.current();\n\n      return clearCycle;\n    }, [clearCycle, isVisible, once]);\n\n    useEffect(() => {\n      if (!isMulti) {\n        textOpacity.set(1);\n        textBlur.set(0);\n        textShift.set(0);\n        previousActiveIndexRef.current = activeIndex;\n        return;\n      }\n\n      if (previousActiveIndexRef.current === activeIndex) {\n        return;\n      }\n\n      previousActiveIndexRef.current = activeIndex;\n      textOpacity.set(0.58);\n      textBlur.set(8);\n      textShift.set(5.5);\n\n      const opacityControls = animate(textOpacity, 1, {\n        duration: 0.26,\n        ease: TEXT_SWAP_EASE,\n      });\n      const blurControls = animate(textBlur, 0, {\n        duration: 0.34,\n        ease: TEXT_SWAP_EASE,\n      });\n      const shiftControls = animate(textShift, 0, {\n        duration: 0.34,\n        ease: TEXT_SWAP_EASE,\n      });\n\n      return () => {\n        opacityControls.stop();\n        blurControls.stop();\n        shiftControls.stop();\n      };\n    }, [activeIndex, isMulti, textBlur, textOpacity, textShift]);\n\n    useEffect(() => clearCycle, [clearCycle]);\n\n    const setRefs = (node: HTMLSpanElement | null) => {\n      spanRef.current = node;\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    };\n\n    return (\n      <motion.span\n        animate={animatedW != null ? { width: animatedW } : undefined}\n        className={cn(\n          componentThemeClassName,\n          \"align-bottom text-inherit leading-[100%]\",\n          className\n        )}\n        ref={setRefs}\n        style={containerStyle}\n        transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}\n        {...props}\n      >\n        <motion.span\n          aria-hidden\n          className=\"inline-block text-inherit leading-[100%]\"\n          style={contentStyle}\n        >\n          {texts[activeIndex]}\n        </motion.span>\n        <span className=\"sr-only\">{texts[activeIndex]}</span>\n      </motion.span>\n    );\n  }\n);\n\nDiaTextReveal.displayName = \"DiaTextReveal\";\n\nconst DiaText = DiaTextReveal;\n\nexport { DiaText, DiaTextReveal };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Dia Text",
  "description": "Animated inline text reveal with a sweeping gradient band, repeat controls, and optional fixed-width rotation for motion-driven typography."
}
