{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-streaming-response",
  "title": "AI Streaming Response",
  "description": "Display streaming AI responses with token-by-token animation.",
  "registryDependencies": [
    "ai-message"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ai/ai-streaming-response.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport AIMessage from \"./ai-message\";\n\nconst STREAMING_CONFIG = {\n  TOKEN_DELAY: 30,\n  PAUSE_AFTER_PERIOD: 200,\n  PAUSE_AFTER_COMMA: 100,\n  PAUSE_AFTER_PARAGRAPH: 300,\n} as const;\n\ninterface AIStreamingResponseProps {\n  content: string;\n  onComplete?: () => void;\n  autoStart?: boolean;\n  className?: string;\n}\n\nfunction useTokens(content: string): string[] {\n  const tokens = useMemo(() => {\n    const tokenRegex = /(\\S+|\\s+)/g;\n    return content.match(tokenRegex) || [];\n  }, [content]);\n\n  return tokens;\n}\n\nfunction useStreaming(\n  tokens: string[],\n  autoStart: boolean,\n  onComplete?: () => void\n) {\n  const [displayedTokens, setDisplayedTokens] = useState<string[]>([]);\n  const [isPaused, setIsPaused] = useState(!autoStart);\n  const [isComplete, setIsComplete] = useState(false);\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n  const currentIndexRef = useRef(0);\n\n  useEffect(() => {\n    if (tokens.length === 0) return;\n\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n      timeoutRef.current = null;\n    }\n\n    const timeoutId = setTimeout(() => {\n      setDisplayedTokens([]);\n      setIsComplete(false);\n      setIsPaused(!autoStart);\n    }, 0);\n    currentIndexRef.current = 0;\n\n    return () => clearTimeout(timeoutId);\n  }, [tokens, autoStart]);\n\n  useEffect(() => {\n    if (tokens.length === 0) return;\n    if (isPaused || isComplete || currentIndexRef.current >= tokens.length) {\n      return;\n    }\n\n    const streamNext = () => {\n      if (currentIndexRef.current >= tokens.length) {\n        setIsComplete(true);\n        if (onComplete) {\n          onComplete();\n        }\n        return;\n      }\n\n      const token = tokens[currentIndexRef.current];\n      setDisplayedTokens((prev) => [...prev, token]);\n      currentIndexRef.current += 1;\n\n      const trimmedToken = token.trim();\n\n      let delay = STREAMING_CONFIG.TOKEN_DELAY;\n\n      if (\n        trimmedToken.endsWith(\".\") ||\n        trimmedToken.endsWith(\"!\") ||\n        trimmedToken.endsWith(\"?\")\n      ) {\n        delay += STREAMING_CONFIG.PAUSE_AFTER_PERIOD;\n      } else if (\n        trimmedToken.endsWith(\",\") ||\n        trimmedToken.endsWith(\";\") ||\n        trimmedToken.endsWith(\":\")\n      ) {\n        delay += STREAMING_CONFIG.PAUSE_AFTER_COMMA;\n      } else if (token.includes(\"\\n\\n\")) {\n        delay += STREAMING_CONFIG.PAUSE_AFTER_PARAGRAPH;\n      }\n\n      if (typeof window !== \"undefined\") {\n        const prefersReducedMotion = window.matchMedia(\n          \"(prefers-reduced-motion: reduce)\"\n        ).matches;\n\n        if (prefersReducedMotion) {\n          const remainingTokens = tokens.slice(currentIndexRef.current);\n          setDisplayedTokens((prev) => [...prev, ...remainingTokens]);\n          currentIndexRef.current = tokens.length;\n          setIsComplete(true);\n          if (onComplete) {\n            onComplete();\n          }\n          return;\n        }\n      }\n\n      timeoutRef.current = setTimeout(streamNext, delay);\n    };\n\n    streamNext();\n\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n        timeoutRef.current = null;\n      }\n    };\n  }, [tokens, isPaused, isComplete, onComplete]);\n\n  const pause = () => {\n    setIsPaused(true);\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n  };\n\n  const resume = () => {\n    if (isComplete) return;\n    setIsPaused(false);\n  };\n\n  return {\n    displayedText: displayedTokens.join(\"\"),\n    isPaused,\n    isComplete,\n  };\n}\n\nexport default function AIStreamingResponse({\n  content,\n  onComplete,\n  autoStart = true,\n  className,\n}: AIStreamingResponseProps) {\n  const tokens = useTokens(content);\n  const { displayedText, isPaused, isComplete } = useStreaming(\n    tokens,\n    autoStart,\n    onComplete\n  );\n\n  return (\n    <div className={cn(\"relative\", className)}>\n      <AIMessage\n        className=\"shadow-none\"\n        content={displayedText}\n        isStreaming={false}\n        skipCodeHighlighting={!isComplete}\n      />\n      {!(isComplete || isPaused) && (\n        <span\n          aria-hidden=\"true\"\n          className=\"inline-block h-4 w-0.5 bg-foreground motion-safe:animate-pulse\"\n        />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "ai"
  ],
  "type": "registry:ui"
}