{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-composer",
  "type": "registry:ui",
  "description": "Multiline prompt field with shader avatar, staggered placeholder, loading line, and send/attach chrome",
  "dependencies": [
    "@base-ui/react",
    "@paper-design/shaders-react",
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "path": "registry/default/ui/prompt-composer.tsx",
      "content": "\"use client\"\n\n/**\n * Multiline prompt field matching `HaloSearchInput` polish: animated gradient\n * rim blobs (inlined; no `halo-card` import), soft ambient backdrop orbs, frosted\n * fill, `AiBlobWarpAvatar` + Paper `Warp` shader (inlined; no `halo-search` import),\n * staggered\n * placeholder / loading line, trailing attach + send with the same layout + crossfade\n * as search (send ↔ spinner).\n */\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ChangeEvent,\n  type ComponentProps,\n  type KeyboardEvent,\n  type TextareaHTMLAttributes,\n} from \"react\"\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { Warp, type WarpProps } from \"@paper-design/shaders-react\"\nimport { Paperclip, Send } from \"lucide-react\"\nimport {\n  AnimatePresence,\n  motion,\n  useInView,\n  useReducedMotion,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* ─── Rim blobs (self-contained; no halo-card import) ─── */\n\ntype RimVariant = \"default\" | \"success\" | \"destructive\"\n\nconst RIM_BLOB_GRADIENTS: Record<\n  RimVariant,\n  { primaryBg: string; secondaryBg: string; tertiaryBg: string }\n> = {\n  default: {\n    primaryBg: \"linear-gradient(90deg, #ff0080, #7928ca, #00d4ff, #0070f3)\",\n    secondaryBg: \"linear-gradient(90deg, #ff4d4d, #f9cb28, #ff0080)\",\n    tertiaryBg: \"linear-gradient(90deg, #0070f3, #00d4ff, #7928ca)\",\n  },\n  success: {\n    primaryBg:\n      \"linear-gradient(90deg, #047857, #059669, #10b981, #14b8a6, #0d9488)\",\n    secondaryBg: \"linear-gradient(90deg, #34d399, #6ee7b7, #2dd4bf, #5eead4)\",\n    tertiaryBg: \"linear-gradient(90deg, #065f46, #047857, #0f766e)\",\n  },\n  destructive: {\n    primaryBg:\n      \"linear-gradient(90deg, #991b1b, #dc2626, #e11d48, #f43f5e, #be123c)\",\n    secondaryBg: \"linear-gradient(90deg, #fb7185, #fda4af, #f87171, #fecdd3)\",\n    tertiaryBg: \"linear-gradient(90deg, #7f1d1d, #b91c1c, #dc2626)\",\n  },\n}\n\nfunction composerBorderGlowConfig(\n  translucent: boolean,\n  rimEmphasis: boolean,\n  rimVariant: RimVariant = \"default\"\n) {\n  let idleOpacity: number\n  if (translucent) {\n    idleOpacity = rimEmphasis ? 0.92 : 0.72\n  } else {\n    idleOpacity = rimEmphasis ? 0.78 : 0.5\n  }\n  const gradients = RIM_BLOB_GRADIENTS[rimVariant]\n  return {\n    opacityIdle: idleOpacity,\n    primaryBg: gradients.primaryBg,\n    secondaryBg: gradients.secondaryBg,\n    tertiaryBg: gradients.tertiaryBg,\n    primaryLeft: [\"-5%\", \"75%\", \"-5%\"],\n    secondaryLeft: [\"65%\", \"10%\", \"65%\"],\n    tertiaryLeft: [\"25%\", \"55%\", \"25%\"],\n    durationMain: 6,\n    durationSec: 5,\n    durationTer: 4,\n  }\n}\n\ntype ComposerBorderGlowState = ReturnType<typeof composerBorderGlowConfig>\n\nfunction ComposerCardRimBlobs({\n  borderGlow,\n  reduceMotion,\n}: {\n  borderGlow: ComposerBorderGlowState\n  reduceMotion: boolean\n}) {\n  return (\n    <div className=\"pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-2xl\">\n      <motion.div\n        animate={{\n          opacity: reduceMotion\n            ? borderGlow.opacityIdle * 0.82\n            : borderGlow.opacityIdle,\n        }}\n        className=\"absolute inset-x-0 bottom-0 h-[58%]\"\n        transition={{ duration: 0.25, ease: \"easeOut\" }}\n      >\n        <motion.div\n          animate={{\n            left: borderGlow.primaryLeft,\n          }}\n          className=\"-bottom-10 absolute h-32 w-88 blur-2xl\"\n          style={{\n            background: borderGlow.primaryBg,\n          }}\n          transition={{\n            duration: reduceMotion ? 0.01 : borderGlow.durationMain,\n            repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n            ease: \"easeInOut\",\n          }}\n        />\n        <motion.div\n          animate={{\n            left: borderGlow.secondaryLeft,\n          }}\n          className=\"-bottom-7 absolute h-24 w-52 blur-2xl\"\n          style={{\n            background: borderGlow.secondaryBg,\n          }}\n          transition={{\n            duration: reduceMotion ? 0.01 : borderGlow.durationSec,\n            repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n            ease: \"easeInOut\",\n          }}\n        />\n        <motion.div\n          animate={{\n            left: borderGlow.tertiaryLeft,\n          }}\n          className=\"-bottom-5 absolute h-20 w-44 blur-xl\"\n          style={{\n            background: borderGlow.tertiaryBg,\n          }}\n          transition={{\n            duration: reduceMotion ? 0.01 : borderGlow.durationTer,\n            repeat: reduceMotion ? 0 : Number.POSITIVE_INFINITY,\n            ease: \"easeInOut\",\n          }}\n        />\n      </motion.div>\n    </div>\n  )\n}\n\n/* ─── Shader avatar (inlined; no halo-search import) ─── */\n\nconst DEFAULT_WARP: Partial<WarpProps> = {\n  colors: [\"#fda4af\", \"#0070f3\", \"#fb923c\", \"#f472b6\", \"#00d4ff\"],\n  distortion: 0.25,\n  height: 720,\n  proportion: 0.54,\n  scale: 0.2,\n  shape: \"checks\",\n  shapeScale: 1,\n  softness: 1,\n  speed: 0.1,\n  swirl: 0.8,\n  swirlIterations: 10,\n  width: 1280,\n}\n\nexport type AiBlobWarpAvatarProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  pulseDurationSec?: number\n  warpProps?: Partial<WarpProps>\n}\n\nexport const AiBlobWarpAvatar = forwardRef<\n  HTMLDivElement,\n  AiBlobWarpAvatarProps\n>(function AiBlobWarpAvatarImpl(\n  { className, pulseDurationSec = 5.2, warpProps, ...props },\n  forwardedRef\n) {\n  const {\n    className: _warpClassName,\n    speed: warpSpeed,\n    ...restWarpProps\n  } = warpProps ?? {}\n  const rootRef = useRef<HTMLDivElement>(null)\n  const reduceMotion = useReducedMotion()\n  const inView = useInView(rootRef, {\n    amount: 0.2,\n    margin: \"0px 0px -10% 0px\",\n  })\n  const live = inView && !reduceMotion\n\n  const setRefs = useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node\n      if (typeof forwardedRef === \"function\") {\n        forwardedRef(node)\n      } else if (forwardedRef) {\n        forwardedRef.current = node\n      }\n    },\n    [forwardedRef]\n  )\n\n  const baseSpeed = warpSpeed ?? DEFAULT_WARP.speed ?? 1\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate size-8 overflow-hidden rounded-full bg-muted shadow-foreground/10 shadow-sm ring-1 ring-foreground/10\",\n        className\n      )}\n      ref={setRefs}\n      {...props}\n    >\n      <motion.div\n        animate={\n          live\n            ? {\n                scale: [1, 1.055, 0.99, 1],\n                rotate: [0, 2, -1.5, 0],\n              }\n            : { scale: 1, rotate: 0 }\n        }\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 origin-center rounded-full\"\n        transition={{\n          duration: pulseDurationSec,\n          ease: \"easeInOut\",\n          repeat: Number.POSITIVE_INFINITY,\n        }}\n      >\n        <Warp\n          {...DEFAULT_WARP}\n          {...restWarpProps}\n          speed={live ? baseSpeed : 0}\n        />\n      </motion.div>\n    </div>\n  )\n})\n\nAiBlobWarpAvatar.displayName = \"AiBlobWarpAvatar\"\n\n/** Placeholder line: snappy stagger (matches search). */\nconst PLACEHOLDER_STAGGER_SEC = 0.018\nconst PLACEHOLDER_CHAR_DURATION_SEC = 0.16\n\nconst DEFAULT_LOADING_STAGGER_SEC = 0.038\nconst DEFAULT_LOADING_CHAR_DURATION_SEC = 0.36\n\nconst TRAILING_ACTION_DURATION_SEC = 0.2\n\nconst trailingMotionTransition = (reduceMotion: boolean) => ({\n  duration: reduceMotion ? 0 : TRAILING_ACTION_DURATION_SEC,\n  ease: \"easeOut\" as const,\n})\n\nconst trailingCrossfade = (reduceMotion: boolean) => ({\n  exit: {\n    opacity: reduceMotion ? 1 : 0,\n    scale: reduceMotion ? 1 : 0.88,\n  },\n  initial: {\n    opacity: reduceMotion ? 1 : 0,\n    scale: reduceMotion ? 1 : 0.88,\n  },\n})\n\nfunction FieldSpinner({\n  className,\n  reduceMotion,\n}: {\n  className?: string\n  reduceMotion: boolean\n}) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex origin-center text-muted-foreground\",\n        !reduceMotion && \"animate-spin\",\n        className\n      )}\n    >\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-4 w-4\"\n        focusable=\"false\"\n        viewBox=\"0 0 24 24\"\n      >\n        <circle\n          className=\"opacity-[0.22]\"\n          cx=\"12\"\n          cy=\"12\"\n          fill=\"none\"\n          r=\"9\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.25\"\n        />\n        <circle\n          cx=\"12\"\n          cy=\"12\"\n          fill=\"none\"\n          r=\"9\"\n          stroke=\"currentColor\"\n          strokeDasharray=\"14 42\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"2.25\"\n        />\n      </svg>\n    </span>\n  )\n}\n\nfunction AnimatedPlaceholder({\n  text,\n  animateKey,\n  reduceMotion,\n  staggerSec = PLACEHOLDER_STAGGER_SEC,\n  charDurationSec = PLACEHOLDER_CHAR_DURATION_SEC,\n  className,\n  alignTop,\n}: {\n  text: string\n  animateKey: number\n  reduceMotion: boolean\n  staggerSec?: number\n  charDurationSec?: number\n  className?: string\n  alignTop?: boolean\n}) {\n  const chars = useMemo(() => {\n    const counts = new Map<string, number>()\n    return Array.from(text, (char) => {\n      const count = counts.get(char) ?? 0\n      counts.set(char, count + 1)\n      return { char, key: `${char}-${count}` }\n    })\n  }, [text])\n\n  return (\n    <motion.span\n      animate=\"visible\"\n      className={cn(\n        \"pointer-events-none absolute inset-x-0 text-pretty text-muted-foreground\",\n        alignTop\n          ? \"top-0 flex flex-wrap content-start gap-x-0\"\n          : \"inset-y-0 flex items-center\",\n        className\n      )}\n      initial=\"hidden\"\n      key={animateKey}\n      variants={{\n        hidden: {},\n        visible: {\n          transition: {\n            staggerChildren: reduceMotion ? 0 : staggerSec,\n          },\n        },\n      }}\n    >\n      {chars.map((glyph) => (\n        <motion.span\n          className=\"inline-block whitespace-pre-wrap\"\n          key={glyph.key}\n          transition={{\n            duration: reduceMotion ? 0 : charDurationSec,\n            ease: \"easeOut\",\n          }}\n          variants={{\n            hidden: { opacity: reduceMotion ? 1 : 0 },\n            visible: { opacity: 1 },\n          }}\n        >\n          {glyph.char}\n        </motion.span>\n      ))}\n    </motion.span>\n  )\n}\n\nfunction TrailingLoadingSlot({ reduceMotion }: { reduceMotion: boolean }) {\n  const cross = trailingCrossfade(reduceMotion)\n  return (\n    <motion.div\n      animate={{ opacity: 1, scale: 1 }}\n      aria-hidden\n      className={cn(\n        \"absolute inset-0 flex items-center justify-center rounded-full\",\n        \"pointer-events-none bg-muted text-muted-foreground shadow-foreground/10 shadow-sm ring-1 ring-foreground/10\"\n      )}\n      exit={cross.exit}\n      initial={cross.initial}\n      key=\"composer-trailing-loading\"\n      layoutId=\"prompt-composer-trailing\"\n      transition={trailingMotionTransition(reduceMotion)}\n    >\n      <FieldSpinner reduceMotion={reduceMotion} />\n    </motion.div>\n  )\n}\n\nfunction TrailingSendSlot({\n  reduceMotion,\n  disabled,\n  onClick,\n}: {\n  reduceMotion: boolean\n  disabled: boolean\n  onClick: () => void\n}) {\n  const cross = trailingCrossfade(reduceMotion)\n  return (\n    <motion.div\n      animate={{ opacity: 1, scale: 1 }}\n      className=\"absolute inset-0\"\n      exit={cross.exit}\n      initial={cross.initial}\n      key=\"composer-trailing-send\"\n      layoutId=\"prompt-composer-trailing\"\n      transition={trailingMotionTransition(reduceMotion)}\n    >\n      <ButtonPrimitive\n        className={cn(\n          \"flex size-full min-h-10 min-w-10 select-none items-center justify-center rounded-full outline-none\",\n          \"shadow-foreground/10 shadow-sm ring-1 ring-foreground/10 transition-[background-color,color,transform] duration-150 ease-out\",\n          \"focus-visible:ring-2 focus-visible:ring-ring/35\",\n          \"fine-hover:enabled:hover:bg-accent fine-hover:enabled:hover:text-accent-foreground\",\n          \"enabled:active:scale-[0.96] motion-reduce:enabled:active:scale-100\",\n          disabled\n            ? \"pointer-events-none bg-muted/80 text-muted-foreground opacity-50\"\n            : \"bg-primary text-primary-foreground fine-hover:hover:bg-primary/90\"\n        )}\n        disabled={disabled}\n        onClick={onClick}\n        type=\"button\"\n      >\n        <Send aria-hidden className=\"size-4 shrink-0\" />\n        <span className=\"sr-only\">Send message</span>\n      </ButtonPrimitive>\n    </motion.div>\n  )\n}\n\n/** Large, soft aurora behind the rim — balances the tall textarea without fighting the bottom blobs. */\nfunction AmbientComposerBackdrop({\n  reduceMotion,\n  emphasis,\n}: {\n  reduceMotion: boolean\n  emphasis: boolean\n}) {\n  const drift = reduceMotion\n    ? { duration: 0 }\n    : {\n        duration: 18,\n        repeat: Number.POSITIVE_INFINITY,\n        ease: \"easeInOut\" as const,\n      }\n\n  return (\n    <div className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-2xl\">\n      <motion.div\n        animate={\n          reduceMotion\n            ? { opacity: emphasis ? 0.11 : 0.07 }\n            : {\n                opacity: emphasis ? [0.08, 0.14, 0.08] : [0.05, 0.1, 0.05],\n                x: [\"-4%\", \"5%\", \"-4%\"],\n                y: [\"-2%\", \"3%\", \"-2%\"],\n              }\n        }\n        className=\"-top-32 -left-20 absolute size-88 blur-3xl\"\n        style={{\n          background:\n            \"radial-gradient(ellipse 80% 70% at 35% 35%, rgba(255, 0, 128, 0.28) 0%, rgba(121, 40, 202, 0.14) 45%, transparent 72%)\",\n        }}\n        transition={drift}\n      />\n      <motion.div\n        animate={\n          reduceMotion\n            ? { opacity: emphasis ? 0.1 : 0.06 }\n            : {\n                opacity: emphasis ? [0.07, 0.12, 0.07] : [0.04, 0.09, 0.04],\n                x: [\"5%\", \"-4%\", \"5%\"],\n                y: [\"2%\", \"-2%\", \"2%\"],\n              }\n        }\n        className=\"-top-28 -right-16 absolute size-80 blur-3xl\"\n        style={{\n          background:\n            \"radial-gradient(ellipse 75% 68% at 65% 40%, rgba(0, 212, 255, 0.22) 0%, rgba(0, 112, 243, 0.12) 48%, transparent 70%)\",\n        }}\n        transition={{\n          ...drift,\n          duration: reduceMotion ? 0 : 22,\n        }}\n      />\n      <div\n        aria-hidden\n        className=\"absolute inset-0 bg-[radial-gradient(ellipse_120%_80%_at_50%_100%,transparent_0%,color-mix(in_oklch,var(--background)_88%,transparent)_55%,transparent_100%)] opacity-70\"\n      />\n    </div>\n  )\n}\n\nexport interface PromptComposerProps\n  extends Omit<\n    TextareaHTMLAttributes<HTMLTextAreaElement>,\n    \"onResize\" | \"children\"\n  > {\n  /**\n   * Frosted fill so animated gradient blobs read through more clearly.\n   * Also applied automatically while `isLoading`.\n   */\n  blobTranslucent?: boolean\n  isLoading?: boolean\n  loadingText?: string\n  loadingStaggerSec?: number\n  loadingCharDurationSec?: number\n  /** Fires when the user clicks Send or presses ⌘/Ctrl+Enter (unless `disabled` / loading / empty). */\n  onSend?: () => void\n  /** When set, an attach control is shown (unless `showAttach` is false). */\n  onAttach?: () => void\n  showAttach?: boolean\n  /** Grow height with content up to `maxAutoGrowPx`. */\n  autoGrow?: boolean\n  maxAutoGrowPx?: number\n}\n\nexport const PromptComposer = forwardRef<\n  HTMLTextAreaElement,\n  PromptComposerProps\n>(\n  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: mirrors search/composer surface; splitting would obscure the single-field API.\n  function PromptComposerImpl(\n    {\n      className,\n      value,\n      onChange,\n      placeholder = \"Write a prompt…\",\n      blobTranslucent = false,\n      isLoading = false,\n      loadingText = \"Sending…\",\n      loadingStaggerSec = DEFAULT_LOADING_STAGGER_SEC,\n      loadingCharDurationSec = DEFAULT_LOADING_CHAR_DURATION_SEC,\n      disabled,\n      readOnly,\n      onSend,\n      onAttach,\n      showAttach,\n      autoGrow = true,\n      maxAutoGrowPx = 280,\n      onKeyDown,\n      ...props\n    },\n    forwardedRef\n  ) {\n    const showBlobTranslucent = blobTranslucent || isLoading\n    const [internalValue, setInternalValue] = useState(\"\")\n    const [isFocused, setIsFocused] = useState(false)\n    const [placeholderAnimKey, setPlaceholderAnimKey] = useState(0)\n    const [loadingAnimKey, setLoadingAnimKey] = useState(0)\n    const [isTypingBurst, setIsTypingBurst] = useState(false)\n    const textareaRef = useRef<HTMLTextAreaElement>(null)\n    const typingIdleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(\n      null\n    )\n    const wasLoadingRef = useRef(isLoading)\n    const reduceMotion = useReducedMotion() ?? false\n    const fieldId = useId()\n\n    useEffect(() => {\n      if (isLoading && !wasLoadingRef.current) {\n        setLoadingAnimKey((k) => k + 1)\n      }\n      wasLoadingRef.current = isLoading\n    }, [isLoading])\n\n    const setRefs = useCallback(\n      (node: HTMLTextAreaElement | null) => {\n        textareaRef.current = node\n        if (typeof forwardedRef === \"function\") {\n          forwardedRef(node)\n        } else if (forwardedRef) {\n          forwardedRef.current = node\n        }\n      },\n      [forwardedRef]\n    )\n\n    const currentValue = value === undefined ? internalValue : value\n    const hasValue = String(currentValue).trim().length > 0\n    const canSend = hasValue && !isLoading && !disabled && !readOnly\n\n    const showAttachControl = (showAttach ?? !!onAttach) && !!onAttach\n\n    useEffect(() => {\n      return () => {\n        if (typingIdleTimerRef.current) {\n          clearTimeout(typingIdleTimerRef.current)\n        }\n      }\n    }, [])\n\n    const adjustHeight = useCallback(() => {\n      const el = textareaRef.current\n      if (!(el && autoGrow)) {\n        return\n      }\n      el.style.height = \"auto\"\n      const next = Math.min(el.scrollHeight, maxAutoGrowPx)\n      el.style.height = `${next}px`\n    }, [autoGrow, maxAutoGrowPx])\n\n    // biome-ignore lint/correctness/useExhaustiveDependencies: rerun when controlled `value` changes from parent (not only via onChange).\n    useEffect(() => {\n      adjustHeight()\n    }, [adjustHeight, currentValue])\n\n    const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {\n      const prevLen = String(currentValue).length\n      const nextLen = e.target.value.length\n      if (nextLen === 0 && prevLen > 0) {\n        setPlaceholderAnimKey((k) => k + 1)\n      }\n\n      if (onChange) {\n        onChange(e)\n      } else {\n        setInternalValue(e.target.value)\n      }\n\n      if (!reduceMotion) {\n        if (typingIdleTimerRef.current) {\n          clearTimeout(typingIdleTimerRef.current)\n        }\n        setIsTypingBurst(true)\n        typingIdleTimerRef.current = setTimeout(() => {\n          setIsTypingBurst(false)\n          typingIdleTimerRef.current = null\n        }, 420)\n      }\n    }\n\n    const fireSend = useCallback(() => {\n      if (!canSend) {\n        return\n      }\n      onSend?.()\n    }, [canSend, onSend])\n\n    const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n      onKeyDown?.(e)\n      if (e.defaultPrevented) {\n        return\n      }\n      if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n        e.preventDefault()\n        fireSend()\n      }\n    }\n\n    const borderGlow = useMemo(\n      () => composerBorderGlowConfig(showBlobTranslucent, isFocused),\n      [showBlobTranslucent, isFocused]\n    )\n\n    const { avatarPulseSec, avatarWarpSpeed } = useMemo(() => {\n      if (isLoading) {\n        return { avatarPulseSec: 1.65, avatarWarpSpeed: 3.45 }\n      }\n      if (isTypingBurst) {\n        return { avatarPulseSec: 2.65, avatarWarpSpeed: 2.65 }\n      }\n      return { avatarPulseSec: 5.2, avatarWarpSpeed: 1.15 }\n    }, [isLoading, isTypingBurst])\n\n    return (\n      <div className={cn(\"relative w-full max-w-2xl\", className)}>\n        <div className=\"relative rounded-2xl p-px\">\n          <AmbientComposerBackdrop\n            emphasis={isFocused || showBlobTranslucent}\n            reduceMotion={reduceMotion}\n          />\n          <ComposerCardRimBlobs\n            borderGlow={borderGlow}\n            reduceMotion={reduceMotion}\n          />\n\n          <div\n            aria-busy={isLoading || undefined}\n            className={cn(\n              \"relative z-10 flex flex-col gap-2 rounded-2xl px-4 py-3\",\n              \"border border-border\",\n              \"shadow-black/5 shadow-sm dark:shadow-black/30\",\n              \"transition-[background,backdrop-filter,border-color,box-shadow] duration-200\",\n              isFocused && \"border-ring/70 dark:border-zinc-500/45\",\n              showBlobTranslucent && \"backdrop-blur-xl backdrop-saturate-150\"\n            )}\n            style={{\n              background: showBlobTranslucent\n                ? \"linear-gradient(to bottom, color-mix(in oklch, var(--card) 82%, transparent) 0%, color-mix(in oklch, var(--card) 70%, transparent) 100%)\"\n                : \"linear-gradient(to bottom, var(--card) 0%, color-mix(in oklch, var(--card) 94%, var(--muted)) 100%)\",\n            }}\n          >\n            <div className=\"flex min-h-0 gap-3\">\n              <AiBlobWarpAvatar\n                className=\"mt-0.5 shrink-0\"\n                pulseDurationSec={avatarPulseSec}\n                warpProps={{ speed: avatarWarpSpeed }}\n              />\n\n              <div className=\"relative min-h-[7.5rem] min-w-0 flex-1\">\n                <textarea\n                  {...props}\n                  aria-label={props[\"aria-label\"] ?? \"Message\"}\n                  className={cn(\n                    \"w-full resize-none bg-transparent text-base text-foreground leading-relaxed antialiased outline-none\",\n                    \"caret-foreground placeholder:text-muted-foreground\",\n                    (isLoading || disabled) &&\n                      \"text-transparent caret-transparent\"\n                  )}\n                  disabled={disabled}\n                  id={props.id ?? fieldId}\n                  onBlur={() => setIsFocused(false)}\n                  onChange={(e) => {\n                    handleChange(e)\n                    queueMicrotask(adjustHeight)\n                  }}\n                  onFocus={() => setIsFocused(true)}\n                  onKeyDown={handleKeyDown}\n                  placeholder=\"\"\n                  readOnly={readOnly || isLoading}\n                  ref={setRefs}\n                  rows={props.rows ?? 4}\n                  value={currentValue}\n                />\n                {isLoading && (\n                  <AnimatedPlaceholder\n                    alignTop\n                    animateKey={loadingAnimKey}\n                    charDurationSec={loadingCharDurationSec}\n                    className=\"pr-1\"\n                    reduceMotion={reduceMotion}\n                    staggerSec={loadingStaggerSec}\n                    text={loadingText}\n                  />\n                )}\n                {!(hasValue || isLoading) && (\n                  <AnimatedPlaceholder\n                    alignTop\n                    animateKey={placeholderAnimKey}\n                    className=\"pr-1\"\n                    reduceMotion={reduceMotion}\n                    text={placeholder}\n                  />\n                )}\n              </div>\n            </div>\n\n            <div className=\"flex items-center justify-end gap-2 border-border/50 border-t pt-2\">\n              {showAttachControl ? (\n                <ButtonPrimitive\n                  className={cn(\n                    \"flex min-h-10 min-w-10 shrink-0 select-none items-center justify-center rounded-full outline-none\",\n                    \"bg-muted text-muted-foreground shadow-foreground/10 shadow-sm ring-1 ring-foreground/10\",\n                    \"transition-[background-color,color,transform] duration-150 ease-out\",\n                    \"fine-hover:hover:bg-accent fine-hover:hover:text-accent-foreground\",\n                    \"focus-visible:ring-2 focus-visible:ring-ring/35\",\n                    \"enabled:active:scale-[0.96] motion-reduce:enabled:active:scale-100\",\n                    (disabled || isLoading) && \"pointer-events-none opacity-40\"\n                  )}\n                  disabled={disabled || isLoading}\n                  onClick={onAttach}\n                  type=\"button\"\n                >\n                  <Paperclip aria-hidden className=\"size-4 shrink-0\" />\n                  <span className=\"sr-only\">Attach file</span>\n                </ButtonPrimitive>\n              ) : null}\n\n              <div className=\"relative size-10 shrink-0\">\n                <AnimatePresence initial={false} mode=\"popLayout\">\n                  {isLoading ? (\n                    <TrailingLoadingSlot reduceMotion={reduceMotion} />\n                  ) : (\n                    <TrailingSendSlot\n                      disabled={!canSend}\n                      onClick={fireSend}\n                      reduceMotion={reduceMotion}\n                    />\n                  )}\n                </AnimatePresence>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n)\n\nexport type PromptTextareaProps = PromptComposerProps\n\n/** Alias for consumers that think of this surface as a textarea first. */\nexport { PromptComposer as PromptTextarea }\n",
      "type": "registry:ui"
    }
  ]
}