{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-input",
  "title": "AI Input",
  "description": "A animated gradient border AI input component.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "textarea"
  ],
  "files": [
    {
      "path": "registry/new-york/ui/ai-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ArrowUp, Paperclip } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  motion,\n  useAnimationFrame,\n  useMotionValue,\n  useTransform,\n} from \"motion/react\";\n\nexport interface AIInputProps {\n  value?: string;\n  onChange?: (value: string) => void;\n  onSubmit?: (value: string) => void;\n  onFileAttach?: () => void;\n  placeholder?: string;\n  disabled?: boolean;\n  loading?: boolean;\n  maxLength?: number;\n  background?: string;\n  className?: string;\n}\n\nfunction AnimatedBorder({ children }: { children: React.ReactNode }) {\n  const angle = useMotionValue(0);\n\n  useAnimationFrame((_, delta) => {\n    angle.set((angle.get() + delta * 0.1) % 360);\n  });\n\n  const conicGradient = useTransform(angle, (a) => {\n    const deg = a % 360;\n    return `conic-gradient(from ${deg}deg, transparent 0deg, transparent 270deg, #a855f7 290deg, #22d3ee 320deg, #f472b6 340deg, transparent 360deg)`;\n  });\n\n  return (\n    <>\n      <motion.div\n        aria-hidden=\"true\"\n        suppressHydrationWarning\n        style={{\n          background: conicGradient,\n          position: \"absolute\",\n          inset: -10,\n          borderRadius: 22,\n          filter: \"blur(16px)\",\n          zIndex: 0,\n          pointerEvents: \"none\",\n        }}\n        className=\"opacity-70\"\n      />\n\n      <motion.div\n        suppressHydrationWarning\n        style={{\n          background: conicGradient,\n          borderRadius: 16,\n          padding: 1.5,\n          position: \"relative\",\n          zIndex: 10,\n        }}\n      >\n        {children}\n      </motion.div>\n    </>\n  );\n}\n\nexport function AIInput({\n  value: controlledValue,\n  onChange,\n  onSubmit,\n  onFileAttach,\n  placeholder = \"Ask anything…\",\n  disabled = false,\n  loading = false,\n  maxLength,\n  background,\n  className,\n}: AIInputProps) {\n  const [internalValue, setInternalValue] = React.useState(\"\");\n  const isControlled = controlledValue !== undefined;\n  const value = isControlled ? controlledValue : internalValue;\n\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n\n  const [mounted, setMounted] = React.useState(false);\n\n  React.useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const resizeTextarea = React.useCallback(() => {\n    const el = textareaRef.current;\n    if (!el) return;\n\n    requestAnimationFrame(() => {\n      el.style.height = \"auto\";\n      el.style.height = `${el.scrollHeight}px`;\n    });\n  }, []);\n\n  React.useEffect(() => {\n    resizeTextarea();\n  }, [value, resizeTextarea]);\n\n  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    const next = e.target.value;\n\n    if (!isControlled) {\n      setInternalValue(next);\n    }\n\n    onChange?.(next);\n    resizeTextarea();\n  };\n\n  const handleSubmit = React.useCallback(() => {\n    const trimmed = value.trim();\n\n    if (!trimmed || disabled || loading) return;\n\n    onSubmit?.(trimmed);\n\n    if (!isControlled) {\n      setInternalValue(\"\");\n\n      requestAnimationFrame(() => {\n        if (textareaRef.current) {\n          textareaRef.current.style.height = \"auto\";\n        }\n      });\n    }\n  }, [value, disabled, loading, isControlled, onSubmit]);\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      handleSubmit();\n    }\n  };\n\n  const canSend = value.trim().length > 0 && !disabled && !loading;\n\n  const defaultBg = background ?? undefined;\n\n  const inner = (\n    <div\n      style={\n        defaultBg\n          ? {\n              background: defaultBg,\n              borderRadius: \"14.5px\",\n            }\n          : {\n              borderRadius: \"14.5px\",\n            }\n      }\n      className={cn(\n        \"flex flex-col p-1.5\",\n        !defaultBg &&\n          \"bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800\"\n      )}\n    >\n      <Textarea\n        ref={textareaRef}\n        value={value}\n        onChange={handleChange}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n        disabled={disabled}\n        maxLength={maxLength}\n        rows={1}\n        aria-label=\"AI chat input\"\n        aria-multiline=\"true\"\n        className={cn(\n          \"w-full resize-none border-0 shadow-none\",\n          \"bg-white dark:bg-neutral-950\",\n          \"rounded-[10px]\",\n          \"px-3 pt-3 pb-2 min-h-0 max-h-48 overflow-y-auto\",\n          \"text-[15px] leading-relaxed\",\n          \"text-neutral-900 dark:text-neutral-100\",\n          \"placeholder:text-neutral-500 dark:placeholder:text-neutral-500\",\n          \"focus-visible:ring-0 focus-visible:ring-offset-0\",\n          \"font-light tracking-[-0.01em]\",\n          \"disabled:opacity-40 disabled:cursor-not-allowed\"\n        )}\n      />\n\n      <div className=\"flex items-center justify-between pt-1 pb-0\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          aria-label=\"Attach file\"\n          disabled={disabled || loading}\n          onClick={onFileAttach}\n          className={cn(\n            \"h-8 w-8 rounded-full transition-colors duration-200\",\n            \"text-neutral-500 hover:text-neutral-700 hover:bg-neutral-200\",\n            \"dark:text-neutral-400 dark:hover:text-neutral-200 dark:hover:bg-neutral-800\",\n            \"disabled:opacity-40 disabled:cursor-not-allowed\"\n          )}\n        >\n          <Paperclip className=\"h-[15px] w-[15px]\" />\n        </Button>\n\n        <Button\n          type=\"button\"\n          size=\"icon\"\n          aria-label={loading ? \"Sending…\" : \"Send message\"}\n          disabled={!canSend}\n          onClick={handleSubmit}\n          className={cn(\n            \"h-8 w-8 rounded-full transition-all duration-200 active:scale-95\",\n            \"bg-neutral-900 text-white hover:bg-black\",\n            \"dark:bg-neutral-100 dark:text-black dark:hover:bg-white\",\n            \"disabled:opacity-25 disabled:cursor-not-allowed\"\n          )}\n        >\n          {loading ? (\n            <svg\n              className=\"h-[15px] w-[15px] animate-spin\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              aria-hidden=\"true\"\n            >\n              <circle\n                className=\"opacity-25\"\n                cx=\"12\"\n                cy=\"12\"\n                r=\"10\"\n                stroke=\"currentColor\"\n                strokeWidth=\"4\"\n              />\n              <path\n                className=\"opacity-75\"\n                fill=\"currentColor\"\n                d=\"M4 12a8 8 0 018-8v4l3-3-3-3V0a12 12 0 00-12 12h4z\"\n              />\n            </svg>\n          ) : (\n            <ArrowUp className=\"h-[15px] w-[15px]\" />\n          )}\n        </Button>\n      </div>\n    </div>\n  );\n\n  const staticBorder = (\n    <div\n      style={{\n        borderRadius: 16,\n        padding: 1.5,\n        position: \"relative\",\n        zIndex: 10,\n        background:\n          \"conic-gradient(from 0deg, transparent 0deg, transparent 270deg, #a855f7 290deg, #22d3ee 320deg, #f472b6 340deg, transparent 360deg)\",\n      }}\n    >\n      {inner}\n    </div>\n  );\n\n  return (\n    <div\n      className={cn(\n        \"relative w-full max-w-xl mx-auto px-2 sm:px-0\",\n        className\n      )}\n    >\n      {mounted ? <AnimatedBorder>{inner}</AnimatedBorder> : staticBorder}\n    </div>\n  );\n}\n\nexport default AIInput;",
      "type": "registry:ui",
      "target": "components/untld/ui/ai-input.tsx"
    }
  ],
  "type": "registry:ui"
}