{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"suggestions","title":"Suggestions","description":"Prompt suggestion chips for guiding user input","dependencies":["@radix-ui/react-slot"],"registryDependencies":["button"],"files":[{"path":"components/nexus-ui/suggestions.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { Presence } from \"@radix-ui/react-presence\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nconst suggestionVariants = cva(\n  \"h-8 gap-1.5 rounded-full px-4 text-sm font-normal shadow-none outline-0 transition-all duration-150 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.99]\",\n  {\n    variants: {\n      variant: {\n        filled:\n          \"border-none bg-muted text-primary hover:bg-border\",\n        outline:\n          \"border border-input bg-transparent text-primary hover:bg-muted\",\n        ghost:\n          \"border-none bg-transparent text-muted-foreground hover:bg-muted hover:text-primary\",\n      },\n    },\n    defaultVariants: {\n      variant: \"filled\",\n    },\n  },\n);\n\ntype SuggestionsContextValue = {\n  onSelect?: (value: string) => void;\n};\n\nconst SuggestionsContext = React.createContext<SuggestionsContextValue>({});\n\ntype SuggestionsProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onSelect\"\n> & {\n  onSelect?: (value: string) => void;\n};\n\nfunction Suggestions({ className, onSelect, ...props }: SuggestionsProps) {\n  return (\n    <SuggestionsContext.Provider value={{ onSelect }}>\n      <div\n        data-slot=\"suggestions\"\n        role=\"group\"\n        aria-label=\"Suggestions\"\n        className={cn(\"flex flex-col gap-2\", className)}\n        {...props}\n      />\n    </SuggestionsContext.Provider>\n  );\n}\n\ntype SuggestionListProps = React.HTMLAttributes<HTMLDivElement> & {\n  orientation?: \"horizontal\" | \"vertical\";\n};\n\nfunction SuggestionList({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: SuggestionListProps) {\n  return (\n    <div\n      data-slot=\"suggestion-list\"\n      role=\"group\"\n      aria-label=\"Suggestions\"\n      className={cn(\n        \"flex gap-2 duration-150\",\n        orientation === \"horizontal\"\n          ? \"flex-row flex-wrap items-center justify-center\"\n          : \"flex-col items-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype SuggestionProps = Omit<React.ComponentProps<typeof Button>, \"variant\"> &\n  VariantProps<typeof suggestionVariants> & {\n    value?: string;\n    highlight?: string | string[];\n  };\n\nfunction highlightText(\n  text: string,\n  terms: string | string[],\n): React.ReactNode {\n  const termList = Array.isArray(terms) ? terms : [terms];\n  const escaped = termList.map((t) => t.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n  const pattern = new RegExp(`(${escaped.join(\"|\")})`, \"gi\");\n  const parts = text.split(pattern);\n\n  return (\n    <span>\n      {parts.map((part, i) =>\n        escaped.some((e) => new RegExp(`^${e}$`, \"i\").test(part)) ? (\n          <span key={i} className=\"text-muted-foreground\">\n            {part}\n          </span>\n        ) : (\n          <span key={i} className=\"text-secondary-foreground\">\n            {part}\n          </span>\n        ),\n      )}\n    </span>\n  );\n}\n\nfunction Suggestion({\n  className,\n  value,\n  variant = \"filled\",\n  highlight,\n  onClick,\n  children,\n  ...props\n}: SuggestionProps) {\n  const { onSelect } = React.useContext(SuggestionsContext);\n\n  const textToHighlight =\n    typeof children === \"string\" ? children : (value ?? \"\");\n  const nonStringChildren = React.Children.toArray(children).filter(\n    (c) => typeof c !== \"string\",\n  );\n  const rendered =\n    highlight && textToHighlight ? (\n      <>\n        {highlightText(textToHighlight, highlight)}\n        {nonStringChildren}\n      </>\n    ) : (\n      children\n    );\n\n  return (\n    <Button\n      data-slot=\"suggestion\"\n      className={cn(suggestionVariants({ variant }), className)}\n      onClick={(e) => {\n        onClick?.(e);\n        const text = value ?? (typeof children === \"string\" ? children : \"\");\n        if (text && onSelect) onSelect(text);\n      }}\n      {...props}\n    >\n      {rendered}\n    </Button>\n  );\n}\n\nconst FOCUSABLE =\n  'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n  return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nconst SuggestionPanelContext = React.createContext<{\n  onOpenChange: (open: boolean) => void;\n} | null>(null);\n\ntype SuggestionPanelProps = React.ComponentProps<\"div\"> & {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onClose?: () => void;\n};\n\nfunction SuggestionPanel({\n  className,\n  open = true,\n  onOpenChange,\n  onClose,\n  ref,\n  children,\n  ...props\n}: SuggestionPanelProps) {\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  const mergedRef = React.useMemo(\n    () => (node: HTMLDivElement | null) => {\n      (panelRef as React.MutableRefObject<HTMLDivElement | null>).current =\n        node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref)\n        (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\n    },\n    [ref],\n  );\n\n  const handleOpenChange = React.useCallback(\n    (next: boolean) => {\n      onOpenChange?.(next);\n    },\n    [onOpenChange],\n  );\n\n  const handleAnimationEnd = React.useCallback(\n    (e: React.AnimationEvent) => {\n      if (e.animationName === \"exit\" && !open) onClose?.();\n    },\n    [open, onClose],\n  );\n\n  React.useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") handleOpenChange(false);\n    };\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [handleOpenChange]);\n\n  React.useEffect(() => {\n    if (!open) return;\n    const panel = panelRef.current;\n    if (!panel) return;\n    const focusable = getFocusableElements(panel);\n    if (focusable.length > 0) focusable[0].focus();\n  }, [open]);\n\n  React.useEffect(() => {\n    const panel = panelRef.current;\n    if (!panel) return;\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key !== \"Tab\") return;\n      const focusable = getFocusableElements(panel);\n      if (focusable.length === 0) return;\n\n      const first = focusable[0];\n      const last = focusable[focusable.length - 1];\n      const active = document.activeElement as HTMLElement | null;\n\n      if (e.shiftKey) {\n        if (active === first) {\n          e.preventDefault();\n          last.focus();\n        }\n      } else {\n        if (active === last) {\n          e.preventDefault();\n          first.focus();\n        }\n      }\n    };\n\n    panel.addEventListener(\"keydown\", handleKeyDown);\n    return () => panel.removeEventListener(\"keydown\", handleKeyDown);\n  }, []);\n\n  const ctx = React.useMemo(\n    () => ({ onOpenChange: handleOpenChange }),\n    [handleOpenChange],\n  );\n\n  return (\n    <Presence present={open}>\n      <div\n        ref={mergedRef}\n        data-slot=\"suggestion-panel\"\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label=\"Suggestions panel\"\n        data-state={open ? \"open\" : \"closed\"}\n        onAnimationEnd={handleAnimationEnd}\n        className={cn(\n          \"rounded-t-0 absolute inset-x-0 -top-7.5 z-0 mx-auto flex w-[calc(100%-16px)] flex-col items-center justify-center gap-3 rounded-b-2xl bg-muted px-2 py-3 duration-200 data-[state=closed]:animate-out data-[state=closed]:duration-0 data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-top-2\",\n          className,\n        )}\n        {...props}\n      >\n        <SuggestionPanelContext.Provider value={ctx}>\n          {children}\n        </SuggestionPanelContext.Provider>\n      </div>\n    </Presence>\n  );\n}\n\nfunction SuggestionPanelHeader({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      data-slot=\"suggestion-panel-header\"\n      className={cn(\"flex w-full items-center justify-between px-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SuggestionPanelTitle({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      data-slot=\"suggestion-panel-title\"\n      className={cn(\"flex items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\ntype SuggestionPanelCloseProps =\n  React.ButtonHTMLAttributes<HTMLButtonElement> & {\n    asChild?: boolean;\n  };\n\nfunction SuggestionPanelClose({\n  asChild = false,\n  className,\n  onClick,\n  \"aria-label\": _ariaLabel,\n  ...props\n}: SuggestionPanelCloseProps) {\n  const ctx = React.useContext(SuggestionPanelContext);\n  const Comp = asChild ? Slot : \"button\";\n\n  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n    ctx?.onOpenChange(false);\n    onClick?.(e);\n  };\n\n  return (\n    <Comp\n      type={asChild ? undefined : \"button\"}\n      data-slot=\"suggestion-panel-close\"\n      aria-label=\"Close suggestions panel\"\n      className={cn(\n        \"flex cursor-pointer items-center justify-center text-muted-foreground hover:text-primary dark:hover:text-primary\",\n        className,\n      )}\n      onClick={handleClick}\n      {...props}\n    />\n  );\n}\n\ntype SuggestionPanelContentProps = React.HTMLAttributes<HTMLDivElement> & {\n  asChild?: boolean;\n};\n\nfunction SuggestionPanelContent({\n  asChild = false,\n  className,\n  ...props\n}: SuggestionPanelContentProps) {\n  const Comp = asChild ? Slot : \"div\";\n  return (\n    <Comp\n      data-slot=\"suggestion-panel-content\"\n      className={cn(\"w-full\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Suggestions,\n  SuggestionList,\n  Suggestion,\n  SuggestionPanel,\n  SuggestionPanelHeader,\n  SuggestionPanelTitle,\n  SuggestionPanelClose,\n  SuggestionPanelContent,\n};\n","type":"registry:file","target":"~/components/nexus-ui/suggestions.tsx"}],"type":"registry:ui"}