{ "$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\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\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\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(\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(null)\n const inputRef = useRef(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 \n {\n if (!showFeedback) {\n openFeedback()\n }\n }}\n className={cn(\n \"relative flex flex-col items-center bottom-8 z-10 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 hover:brightness-105 transition-[filter] duration-200\"\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 \n \n \n \n \n \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
\n ) : (\n \n \n {success && (\n \n \n \n )}\n \n \n )}\n \n )\n\n const defaultTrigger = (\n \n {triggerIcon && {triggerIcon}}\n {triggerLabel}\n \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
\n
\n
\n {indicatorElement}\n
Morph Surface
\n
\n {triggerElement}\n
\n
\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(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) {\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) {\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
\n

\n {triggerLabel}\n

\n \n \n Enter\n \n
\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 \n \n {showFeedback && (\n \n {contentElement}\n \n )}\n \n {showFeedback && (\n \n )}\n \n )\n})\n\nMorphSurfaceFeedback.displayName = \"MorphSurfaceFeedback\"\n\n// Utility components\nfunction IconCheck() {\n return (\n \n Icon Check\n \n \n )\n}\n\nfunction Kbd({\n children,\n className,\n}: {\n children: string\n className?: string\n}) {\n return (\n \n {children}\n \n )\n}\n\nfunction useClickOutside(\n ref: RefObject,\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" } ] }