{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A FigmaComment component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\nimport { getImageKitUrl } from \"@/lib/smoothui-data\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useClickOutside } from \"./use-click-outside\";\n\nconst CLOSED_SIZE = 32;\nconst AVATAR_CLOSED_LEFT = 4;\nconst AVATAR_CLOSED_TOP = 4;\nconst AVATAR_OPEN_LEFT = 12;\nconst AVATAR_OPEN_TOP = 12;\nconst CONTENT_DELAY = 0.15;\nconst INITIAL_BLUR_PX = 6;\nconst EXIT_BLUR_PX = 3;\nconst MEASURE_DELAY_SHORT = 100;\nconst MEASURE_DELAY_LONG = 500;\nconst CONTAINER_CLOSE_DELAY = 0.08;\nconst _BLUR_DURATION = 0.4;\nconst BLUR_EASE_X1 = 0.22;\nconst BLUR_EASE_Y1 = 1;\nconst BLUR_EASE_X2 = 0.36;\nconst BLUR_EASE_Y2 = 1;\nconst BLUR_EASE: [number, number, number, number] = [\n  BLUR_EASE_X1,\n  BLUR_EASE_Y1,\n  BLUR_EASE_X2,\n  BLUR_EASE_Y2,\n];\n\nexport interface FigmaCommentProps {\n  authorName?: string;\n  avatarAlt?: string;\n  avatarUrl?: string;\n  className?: string;\n  message?: string;\n  onOpenChange?: (isOpen: boolean) => void;\n  timestamp?: string;\n  width?: number;\n}\n\nexport default function FigmaComment({\n  avatarUrl = getImageKitUrl(\n    \"https://ik.imagekit.io/16u211libb/avatar-educalvolpz.jpeg?updatedAt=1765524159631\",\n    {\n      format: \"auto\",\n      height: 48,\n      quality: 85,\n      width: 48,\n    }\n  ),\n  avatarAlt = \"Avatar\",\n  className,\n  authorName = \"Edu Calvo\",\n  timestamp = \"Just now\",\n  message = \"What happens if we adjust this to handle a light and dark mode? I'm not sure if we're ready to handle...\",\n  width = 180,\n  onOpenChange,\n}: FigmaCommentProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(CLOSED_SIZE);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Close comment when clicking outside\n  useClickOutside(containerRef, () => {\n    if (isOpen) {\n      setIsOpen(false);\n    }\n  });\n\n  // Notify parent of open state changes\n  useEffect(() => {\n    onOpenChange?.(isOpen);\n  }, [isOpen, onOpenChange]);\n\n  // Measure content height when component mounts or message changes\n  useEffect(() => {\n    const measureHeight = () => {\n      if (contentRef.current) {\n        const innerDiv = contentRef.current.firstElementChild as HTMLElement;\n        if (innerDiv) {\n          const height = innerDiv.scrollHeight;\n          if (height > 0) {\n            setContentHeight(height);\n          }\n        }\n      }\n    };\n\n    // Use setTimeout to ensure DOM is fully rendered\n    const timeoutId = setTimeout(measureHeight, MEASURE_DELAY_SHORT);\n    const timeoutId2 = setTimeout(measureHeight, MEASURE_DELAY_LONG);\n    return () => {\n      clearTimeout(timeoutId);\n      clearTimeout(timeoutId2);\n    };\n  }, []);\n\n  const handleToggle = () => {\n    setIsOpen((prev) => !prev);\n  };\n\n  return (\n    <div className={cn(\"relative\", className)}>\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? {}\n            : {\n                height: isOpen ? contentHeight : CLOSED_SIZE,\n                width: isOpen ? width : CLOSED_SIZE,\n              }\n        }\n        className=\"absolute bottom-0 left-0 cursor-pointer overflow-hidden rounded-2xl rounded-bl-none bg-background shadow-[0px_0px_0.5px_0px_rgba(0,0,0,0.18),0px_3px_8px_0px_rgba(0,0,0,0.1),0px_1px_3px_0px_rgba(0,0,0,0.1)]\"\n        onClick={handleToggle}\n        ref={containerRef}\n        style={\n          shouldReduceMotion\n            ? {\n                height: isOpen ? contentHeight : CLOSED_SIZE,\n                width: isOpen ? width : CLOSED_SIZE,\n              }\n            : undefined\n        }\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 45,\n                delay: isOpen ? 0 : CONTAINER_CLOSE_DELAY,\n                duration: 0.25,\n                mass: 0.7,\n                stiffness: 550,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        {/* Avatar - animates position */}\n        <motion.div\n          animate={\n            shouldReduceMotion\n              ? {}\n              : {\n                  left: isOpen ? AVATAR_OPEN_LEFT : AVATAR_CLOSED_LEFT,\n                  top: isOpen ? AVATAR_OPEN_TOP : AVATAR_CLOSED_TOP,\n                }\n          }\n          className=\"absolute z-10\"\n          style={\n            shouldReduceMotion\n              ? {\n                  left: isOpen ? AVATAR_OPEN_LEFT : AVATAR_CLOSED_LEFT,\n                  top: isOpen ? AVATAR_OPEN_TOP : AVATAR_CLOSED_TOP,\n                }\n              : undefined\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  damping: 25,\n                  duration: 0.25,\n                  stiffness: 300,\n                  type: \"spring\" as const,\n                }\n          }\n        >\n          <Avatar className=\"h-6 w-6\">\n            <AvatarImage alt={avatarAlt} src={avatarUrl} />\n            <AvatarFallback>{authorName.charAt(0)}</AvatarFallback>\n          </Avatar>\n        </motion.div>\n\n        {/* Content - always rendered but hidden when closed for measurement */}\n        <div\n          className=\"pointer-events-none absolute\"\n          ref={contentRef}\n          style={{\n            left: 0,\n            position: \"absolute\",\n            top: \"-9999px\",\n            width: `${width}px`,\n          }}\n        >\n          <div className=\"flex flex-col items-start gap-0.5 py-3 pr-4 pl-11\">\n            {/* Attribution */}\n            <div className=\"flex items-start gap-0.5\">\n              <p className=\"font-semibold text-[11px] text-foreground leading-4\">\n                {authorName}\n              </p>\n              <p className=\"font-medium text-[11px] text-muted-foreground leading-4\">\n                {timestamp}\n              </p>\n            </div>\n            {/* Message */}\n            <p className=\"text-left font-medium text-[11px] text-foreground leading-4\">\n              {message}\n            </p>\n          </div>\n        </div>\n\n        {/* Content - visible when open */}\n        <AnimatePresence>\n          {isOpen ? (\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      filter: \"blur(0px)\",\n                      opacity: 1,\n                    }\n              }\n              className=\"absolute inset-0 flex flex-col items-start gap-0.5 py-3 pr-4 pl-11\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      filter: `blur(${String(EXIT_BLUR_PX)}px)`,\n                      opacity: 0,\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 0 }\n                  : {\n                      filter: `blur(${String(INITIAL_BLUR_PX)}px)`,\n                      opacity: 0,\n                    }\n              }\n              style={{\n                width: `${width}px`,\n              }}\n              transition={\n                (shouldReduceMotion\n                  ? { duration: 0 }\n                  : (isExiting: boolean) => ({\n                      filter: {\n                        delay: isExiting ? 0 : CONTENT_DELAY,\n                        duration: 0.25,\n                        ease: BLUR_EASE,\n                      },\n                      opacity: {\n                        delay: isExiting ? 0 : CONTENT_DELAY,\n                        duration: 0.25,\n                        ease: BLUR_EASE,\n                      },\n                    })) as import(\"motion/react\").Transition\n              }\n            >\n              {/* Attribution */}\n              <div className=\"flex items-start gap-0.5\">\n                <p className=\"font-semibold text-[11px] text-foreground leading-4\">\n                  {authorName}\n                </p>\n                <p className=\"font-medium text-[11px] text-muted-foreground leading-4\">\n                  {timestamp}\n                </p>\n              </div>\n              {/* Message */}\n              <p className=\"text-left font-medium text-[11px] text-foreground leading-4\">\n                {message}\n              </p>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/figma-comment/index.tsx","type":"registry:ui"},{"content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\ntype EventType =\n  | \"mousedown\"\n  | \"mouseup\"\n  | \"touchstart\"\n  | \"touchend\"\n  | \"focusin\"\n  | \"focusout\";\n\nexport function useClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null> | RefObject<T | null>[],\n  handler: (event: Event) => void,\n  eventType: EventType = \"mousedown\"\n): void {\n  useEffect(() => {\n    function callback(event: Event) {\n      const target = event.target as Node;\n\n      // Do nothing if the target is not connected element with document\n      if (!target?.isConnected) {\n        return;\n      }\n\n      const isOutside = Array.isArray(ref)\n        ? ref\n            .filter((r) => Boolean(r.current))\n            .every((r) => r.current && !r.current.contains(target))\n        : ref.current && !ref.current.contains(target);\n\n      if (isOutside) {\n        handler(event);\n      }\n    }\n\n    window.addEventListener(eventType, callback);\n\n    return () => {\n      window.removeEventListener(eventType, callback);\n    };\n  }, [ref, handler, eventType]);\n}\n","path":"use-click-outside.tsx","target":"components/smoothui/figma-comment/use-click-outside.tsx","type":"registry:ui"}],"name":"figma-comment","registryDependencies":["avatar","https://smoothui.dev/r/data.json"],"title":"Figma Comment","type":"registry:ui"}