{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morph-surface",
  "type": "registry:ui",
  "description": "A morphing surface component with smooth animations, customizable dimensions, and configurable content",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/default/ui/morph-surface.tsx",
      "content": "\"use client\"\n\nimport React, {\n  createContext,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type RefObject,\n} from \"react\"\nimport { AnimatePresence, motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype SpringConfig = {\n  type: \"spring\"\n  stiffness: number\n  damping: number\n  mass?: number\n  delay?: number\n}\n\nconst SPEED = 1\nconst FEEDBACK_WIDTH = 360\nconst FEEDBACK_HEIGHT = 200\n\n// Props interfaces\ninterface TriggerProps {\n  isOpen: boolean\n  onClick: () => void\n  className?: string\n}\n\ninterface ContentProps {\n  isOpen: boolean\n  onClose: () => void\n  onSubmit: (data: FormData) => void | Promise<void>\n  className?: string\n}\n\ninterface IndicatorProps {\n  success: boolean\n  isOpen: boolean\n  className?: string\n}\n\ninterface MorphSurfaceProps {\n  // Dimensions\n  collapsedWidth?: number | \"auto\"\n  collapsedHeight?: number\n  expandedWidth?: number\n  expandedHeight?: number\n\n  // Animation\n  animationSpeed?: number\n  springConfig?: SpringConfig\n\n  // Content\n  triggerLabel?: string\n  triggerIcon?: React.ReactNode\n  placeholder?: string\n  submitLabel?: string\n\n  // Callbacks\n  onSubmit?: (data: FormData) => void | Promise<void>\n  onOpen?: () => void\n  onClose?: () => void\n  onSuccess?: () => void\n\n  // Controlled state\n  isOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n\n  // Styles\n  className?: string\n  triggerClassName?: string\n  contentClassName?: string\n\n  // Render props\n  renderTrigger?: (props: TriggerProps) => React.ReactNode\n  renderContent?: (props: ContentProps) => React.ReactNode\n  renderIndicator?: (props: IndicatorProps) => React.ReactNode\n}\n\ninterface MorphSurfaceContextValue {\n  showFeedback: boolean\n  success: boolean\n  openFeedback: () => void\n  closeFeedback: () => void\n  // Configurable props\n  triggerLabel: string\n  triggerIcon?: React.ReactNode\n  placeholder: string\n  submitLabel: string\n  onSubmit?: (data: FormData) => void | Promise<void>\n  onOpen?: () => void\n  onClose?: () => void\n  onSuccess?: () => void\n  triggerClassName?: string\n  contentClassName?: string\n  renderTrigger?: (props: TriggerProps) => React.ReactNode\n  renderContent?: (props: ContentProps) => React.ReactNode\n  renderIndicator?: (props: IndicatorProps) => React.ReactNode\n  animationSpeed: number\n  springConfig?: SpringConfig\n  expandedWidth: number\n  expandedHeight: number\n}\n\nconst MorphSurfaceContext = createContext<MorphSurfaceContextValue>(\n  {} as MorphSurfaceContextValue\n)\n\nconst useMorphSurface = () => useContext(MorphSurfaceContext)\n\n// Internal hook logic\nfunction useMorphSurfaceLogic({\n  isOpen: controlledIsOpen,\n  onOpenChange,\n  expandedWidth = FEEDBACK_WIDTH,\n  expandedHeight = FEEDBACK_HEIGHT,\n  collapsedHeight = 44,\n  animationSpeed = SPEED,\n}: {\n  isOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  expandedWidth?: number\n  expandedHeight?: number\n  collapsedHeight?: number\n  animationSpeed?: number\n}) {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const inputRef = useRef<HTMLTextAreaElement | null>(null)\n  const [internalIsOpen, setInternalIsOpen] = useState(false)\n  const [success, setSuccess] = useState(false)\n\n  const isOpen =\n    controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen\n  const showFeedback = isOpen\n\n  function closeFeedback() {\n    if (controlledIsOpen !== undefined) {\n      onOpenChange?.(false)\n    } else {\n      setInternalIsOpen(false)\n    }\n    inputRef.current?.blur()\n  }\n\n  function openFeedback() {\n    if (controlledIsOpen !== undefined) {\n      onOpenChange?.(!isOpen)\n    } else {\n      setInternalIsOpen((prev) => !prev)\n    }\n    if (!showFeedback) {\n      setTimeout(() => {\n        inputRef.current?.focus()\n      })\n    }\n  }\n\n  function setSuccessState(value: boolean) {\n    setSuccess(value)\n  }\n\n  useClickOutside(containerRef, closeFeedback, showFeedback)\n\n  return {\n    containerRef,\n    inputRef,\n    showFeedback,\n    success,\n    openFeedback,\n    closeFeedback,\n    setSuccess: setSuccessState,\n    expandedWidth,\n    expandedHeight,\n    collapsedHeight,\n    animationSpeed,\n  }\n}\n\n// Root component\nexport function MorphSurface({\n  collapsedWidth = FEEDBACK_WIDTH,\n  collapsedHeight = 44,\n  expandedWidth = FEEDBACK_WIDTH,\n  expandedHeight = FEEDBACK_HEIGHT,\n  animationSpeed = SPEED,\n  springConfig,\n  triggerLabel = \"Feedback\",\n  triggerIcon,\n  placeholder = \"What's on your mind?\",\n  submitLabel,\n  onSubmit,\n  onOpen,\n  onClose,\n  onSuccess,\n  isOpen: controlledIsOpen,\n  onOpenChange,\n  className,\n  triggerClassName,\n  contentClassName,\n  renderTrigger,\n  renderContent,\n  renderIndicator,\n}: MorphSurfaceProps = {}) {\n  const hookLogic = useMorphSurfaceLogic({\n    isOpen: controlledIsOpen,\n    onOpenChange,\n    expandedWidth,\n    expandedHeight,\n    collapsedHeight,\n    animationSpeed,\n  })\n\n  const {\n    containerRef,\n    inputRef,\n    showFeedback,\n    success,\n    openFeedback,\n    closeFeedback,\n    setSuccess,\n    expandedWidth: hookExpandedWidth,\n    expandedHeight: hookExpandedHeight,\n    collapsedHeight: hookCollapsedHeight,\n  } = hookLogic\n\n  function onFeedbackSuccess() {\n    closeFeedback()\n    setSuccess(true)\n    setTimeout(() => {\n      setSuccess(false)\n    }, 1500)\n    onSuccess?.()\n  }\n\n  const context = useMemo(\n    () => ({\n      showFeedback,\n      success,\n      openFeedback: () => {\n        openFeedback()\n        onOpen?.()\n      },\n      closeFeedback: () => {\n        closeFeedback()\n        onClose?.()\n      },\n      triggerLabel,\n      triggerIcon,\n      placeholder,\n      submitLabel: submitLabel || \"⌘ Enter\",\n      onSubmit,\n      onOpen,\n      onClose,\n      onSuccess,\n      triggerClassName,\n      contentClassName,\n      renderTrigger,\n      renderContent,\n      renderIndicator,\n      animationSpeed,\n      springConfig,\n      expandedWidth: hookExpandedWidth,\n      expandedHeight: hookExpandedHeight,\n    }),\n    [\n      showFeedback,\n      success,\n      openFeedback,\n      closeFeedback,\n      triggerLabel,\n      triggerIcon,\n      placeholder,\n      submitLabel,\n      onSubmit,\n      onOpen,\n      onClose,\n      onSuccess,\n      triggerClassName,\n      contentClassName,\n      renderTrigger,\n      renderContent,\n      renderIndicator,\n      animationSpeed,\n      springConfig,\n      hookExpandedWidth,\n      hookExpandedHeight,\n    ]\n  )\n\n  return (\n    <div\n      className={cn(\"flex items-end justify-center\", className)}\n      style={{\n        width: hookExpandedWidth,\n        height: hookExpandedHeight,\n      }}\n    >\n      <motion.div\n        ref={containerRef}\n        onClick={() => {\n          if (!showFeedback) {\n            openFeedback()\n          }\n        }}\n        className={cn(\n          \"relative bottom-8 z-10 flex flex-col items-center overflow-hidden\",\n          \"bg-card dark:bg-muted\",\n          \"shadow-[0px_1px_1px_0px_rgba(0,_0,_0,_0.05),_0px_1px_1px_0px_rgba(255,_252,_240,_0.5)_inset,_0px_0px_0px_1px_hsla(0,_0%,_100%,_0.1)_inset,_0px_0px_1px_0px_rgba(28,_27,_26,_0.5)]\",\n          \"dark:shadow-[0px_1px_0px_0px_hsla(0,_0%,_0%,_0.02)_inset,_0px_0px_0px_1px_hsla(0,_0%,_0%,_0.02)_inset,_0px_0px_0px_1px_rgba(255,_255,_255,_0.25)]\",\n          !showFeedback &&\n            \"cursor-pointer transition-[filter] duration-200 hover:brightness-105\"\n        )}\n        initial={false}\n        animate={{\n          width: showFeedback ? hookExpandedWidth : collapsedWidth,\n          height: showFeedback ? hookExpandedHeight : hookCollapsedHeight,\n          borderRadius: showFeedback ? 14 : 20,\n        }}\n        transition={\n          springConfig || {\n            type: \"spring\",\n            stiffness: 550 / animationSpeed,\n            damping: 45,\n            mass: 0.7,\n            delay: showFeedback ? 0 : 0.08,\n          }\n        }\n      >\n        <MorphSurfaceContext.Provider value={context}>\n          <MorphSurfaceDock />\n          <MorphSurfaceFeedback ref={inputRef} onSuccess={onFeedbackSuccess} />\n        </MorphSurfaceContext.Provider>\n      </motion.div>\n    </div>\n  )\n}\n\n// Dock component\nfunction MorphSurfaceDock() {\n  const {\n    success,\n    showFeedback,\n    openFeedback,\n    triggerLabel,\n    triggerIcon,\n    triggerClassName,\n    renderTrigger,\n    renderIndicator,\n    animationSpeed,\n    springConfig,\n  } = useMorphSurface()\n\n  const logoSpring = springConfig || {\n    type: \"spring\" as const,\n    stiffness: 350 / animationSpeed,\n    damping: 35,\n  }\n\n  const checkSpring = {\n    type: \"spring\" as const,\n    stiffness: 500 / animationSpeed,\n    damping: 22,\n  }\n\n  const handleTriggerClick = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    openFeedback()\n  }\n\n  const defaultIndicator = (\n    <>\n      {showFeedback ? (\n        <div className=\"h-5 w-5\" style={{ opacity: 0 }} />\n      ) : (\n        <motion.div\n          className=\"bg-primary h-5 w-5 rounded-full\"\n          layoutId={`morph-surface-dot-${triggerLabel}`}\n          transition={logoSpring}\n        >\n          <AnimatePresence>\n            {success && (\n              <motion.div\n                key=\"check\"\n                exit={{ opacity: 0, scale: 0.5 }}\n                animate={{ opacity: 1, scale: 1 }}\n                initial={{ opacity: 0, scale: 0.5 }}\n                transition={{\n                  ...checkSpring,\n                  delay: success ? 0.3 : 0,\n                }}\n                className=\"m-[2px]\"\n              >\n                <IconCheck />\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </motion.div>\n      )}\n    </>\n  )\n\n  const defaultTrigger = (\n    <button\n      type=\"button\"\n      className={cn(\n        \"m-[-8px] flex flex-1 justify-end gap-1 rounded-full p-2\",\n        \"text-muted-foreground hover:text-foreground\",\n        \"transition-colors duration-200\",\n        \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n        triggerClassName\n      )}\n      onClick={handleTriggerClick}\n    >\n      {triggerIcon && <span className=\"flex items-center\">{triggerIcon}</span>}\n      <span className=\"ml-1 max-w-[20ch] truncate\">{triggerLabel}</span>\n    </button>\n  )\n\n  const indicatorElement = renderIndicator\n    ? renderIndicator({\n        success,\n        isOpen: showFeedback,\n      })\n    : defaultIndicator\n\n  const triggerElement = renderTrigger\n    ? renderTrigger({\n        isOpen: showFeedback,\n        onClick: () => openFeedback(),\n        className: triggerClassName,\n      })\n    : defaultTrigger\n\n  return (\n    <footer className=\"mt-auto flex h-[44px] items-center justify-center whitespace-nowrap select-none\">\n      <div className=\"flex items-center justify-center gap-6 px-3\">\n        <div className=\"flex w-fit items-center gap-2\">\n          {indicatorElement}\n          <div className=\"text-foreground text-sm\">Morph Surface</div>\n        </div>\n        {triggerElement}\n      </div>\n    </footer>\n  )\n}\n\n// Feedback component\nconst MorphSurfaceFeedback = React.forwardRef<\n  HTMLTextAreaElement,\n  { onSuccess: () => void }\n>(({ onSuccess }, ref) => {\n  const {\n    closeFeedback,\n    showFeedback,\n    placeholder,\n    onSubmit,\n    contentClassName,\n    renderContent,\n    expandedWidth,\n    expandedHeight,\n    animationSpeed,\n    triggerLabel,\n  } = useMorphSurface()\n  const submitRef = React.useRef<HTMLButtonElement>(null)\n\n  const contentSpring = {\n    type: \"spring\" as const,\n    stiffness: 550 / animationSpeed,\n    damping: 45,\n    mass: 0.7,\n  }\n\n  const logoSpring = {\n    type: \"spring\" as const,\n    stiffness: 350 / animationSpeed,\n    damping: 35,\n  }\n\n  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n    e.preventDefault()\n    const formData = new FormData(e.currentTarget)\n\n    if (onSubmit) {\n      try {\n        await onSubmit(formData)\n        onSuccess()\n      } catch (error) {\n        console.error(\"Form submission error:\", error)\n      }\n    } else {\n      onSuccess()\n    }\n  }\n\n  function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {\n    if (e.key === \"Escape\") {\n      closeFeedback()\n    }\n    if (e.key === \"Enter\" && e.metaKey) {\n      e.preventDefault()\n      submitRef.current?.click()\n    }\n  }\n\n  const defaultContent = (\n    <>\n      <div className=\"flex justify-between py-1\">\n        <p className=\"text-muted-foreground z-[2] ml-[25px] flex items-center gap-[6px] text-sm select-none\">\n          {triggerLabel}\n        </p>\n        <button\n          type=\"submit\"\n          ref={submitRef}\n          className={cn(\n            \"mt-1 flex -translate-y-[3px] items-center justify-center gap-1 text-sm\",\n            \"text-muted-foreground right-4 bg-transparent text-center select-none\",\n            \"cursor-pointer rounded-xl pr-1\",\n            \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\"\n          )}\n        >\n          <Kbd>⌘</Kbd>\n          <Kbd className=\"w-fit\">Enter</Kbd>\n        </button>\n      </div>\n      <textarea\n        ref={ref}\n        placeholder={placeholder}\n        name=\"message\"\n        className={cn(\n          \"h-full w-full resize-none scroll-py-2 p-4 text-base outline-none\",\n          \"bg-muted dark:bg-accent rounded-t-xs rounded-b-lg\",\n          \"caret-primary\",\n          \"placeholder:text-muted-foreground\",\n          \"focus-visible:ring-ring/20 focus-visible:ring-1 focus-visible:ring-offset-0\"\n        )}\n        required\n        onKeyDown={onKeyDown}\n        spellCheck={false}\n      />\n    </>\n  )\n\n  const handleContentSubmit = async (data: FormData) => {\n    if (onSubmit) {\n      try {\n        await onSubmit(data)\n        onSuccess()\n      } catch (error) {\n        console.error(\"Form submission error:\", error)\n      }\n    } else {\n      onSuccess()\n    }\n  }\n\n  const contentElement = renderContent\n    ? renderContent({\n        isOpen: showFeedback,\n        onClose: closeFeedback,\n        onSubmit: handleContentSubmit,\n        className: contentClassName,\n      })\n    : defaultContent\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      className={cn(\"absolute bottom-0\", contentClassName)}\n      style={{\n        width: expandedWidth,\n        height: expandedHeight,\n        pointerEvents: showFeedback ? \"all\" : \"none\",\n      }}\n    >\n      <AnimatePresence>\n        {showFeedback && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={contentSpring}\n            className=\"flex h-full flex-col p-1\"\n          >\n            {contentElement}\n          </motion.div>\n        )}\n      </AnimatePresence>\n      {showFeedback && (\n        <motion.div\n          layoutId={`morph-surface-dot-${triggerLabel}`}\n          className=\"bg-primary absolute top-[18.5px] left-4 h-2 w-2 rounded-full\"\n          transition={logoSpring}\n        />\n      )}\n    </form>\n  )\n})\n\nMorphSurfaceFeedback.displayName = \"MorphSurfaceFeedback\"\n\n// Utility components\nfunction IconCheck() {\n  return (\n    <svg\n      width=\"16px\"\n      height=\"16px\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      color=\"white\"\n    >\n      <title>Icon Check</title>\n      <path\n        d=\"M5 13L9 17L19 7\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2px\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  )\n}\n\nfunction Kbd({\n  children,\n  className,\n}: {\n  children: string\n  className?: string\n}) {\n  return (\n    <kbd\n      className={cn(\n        \"bg-muted text-muted-foreground flex h-6 w-6 items-center justify-center rounded border px-[6px] font-sans text-xs\",\n        className\n      )}\n    >\n      {children}\n    </kbd>\n  )\n}\n\nfunction useClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null>,\n  handler: (event: MouseEvent | TouchEvent | PointerEvent) => void,\n  isOpen: boolean\n) {\n  useEffect(() => {\n    let startedOutsideWhileOpen = false\n\n    const isOutside = (event: Event) => {\n      const el = ref?.current\n      return !!el && !el.contains((event?.target as Node) || null)\n    }\n\n    const handlePointerStart = (event: PointerEvent) => {\n      startedOutsideWhileOpen = isOpen && isOutside(event)\n    }\n\n    const handleClick = (event: MouseEvent) => {\n      // Only close if this interaction began outside while the surface was open.\n      if (startedOutsideWhileOpen && isOpen && isOutside(event)) {\n        handler(event)\n      }\n      startedOutsideWhileOpen = false\n    }\n\n    const handleTouchEnd = (event: TouchEvent) => {\n      // Touch interactions may not emit click consistently across browsers.\n      if (startedOutsideWhileOpen && isOpen && isOutside(event)) {\n        handler(event)\n      }\n      startedOutsideWhileOpen = false\n    }\n\n    document.addEventListener(\"pointerdown\", handlePointerStart)\n    document.addEventListener(\"click\", handleClick)\n    document.addEventListener(\"touchend\", handleTouchEnd)\n\n    return () => {\n      document.removeEventListener(\"pointerdown\", handlePointerStart)\n      document.removeEventListener(\"click\", handleClick)\n      document.removeEventListener(\"touchend\", handleTouchEnd)\n    }\n  }, [ref, handler, isOpen])\n}\n",
      "type": "registry:ui"
    }
  ]
}