{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "suggestions",
  "type": "registry:ui",
  "title": "Suggestions",
  "description": "Prompt starters that stagger in once and fill the composer when picked.",
  "categories": [
    "ai-chat"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/suggestions.tsx",
      "content": "\"use client\";\nimport { AnimatePresence, MotionConfig, motion, useReducedMotion, type Variants } from \"motion/react\";\nimport { Children, createContext, isValidElement, useContext, useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ArrowRight } from \"@/lib/icons\";\nimport { ease, spring, stagger } from \"@/lib/motion\";\n\ntype Ctx = {\n  layout: \"chips\" | \"list\";\n  onPick?: (prompt: string) => void;\n  reduce: boolean;\n  group: string;\n  hovered: string | null;\n  setHovered: (id: string | null) => void;\n};\nconst SuggestionsContext = createContext<Ctx | null>(null);\nconst IndexContext = createContext(0);\n\nexport type SuggestionsProps = React.ComponentProps<\"div\"> & {\n  /** \"chips\" wrap, and scroll sideways when the space is narrow. \"list\" stacks full-width rows. */\n  layout?: \"chips\" | \"list\";\n  /** Suggestions are being generated: shows placeholder chips of the same shape. */\n  loading?: boolean;\n  /** Called with a suggestion's prompt when it's picked. Fill your input with it. */\n  onPick?: (prompt: string) => void;\n  /** The group's accessible name. */\n  label?: string;\n};\n\nconst container: Variants = { hidden: {}, show: {} };\n\n/**\n * Prompt suggestions: the ways in when the box is empty, or the next question\n * after an answer. They arrive once with a short stagger; picking one hands its\n * full prompt to you to put in the input.\n */\nexport function Suggestions({ layout = \"chips\", loading = false, onPick, label = \"Suggestions\", className, children, ...rest }: SuggestionsProps) {\n  const reduce = !!useReducedMotion();\n  const group = useId();\n  const [hovered, setHovered] = useState<string | null>(null);\n  const row = useRef<HTMLUListElement>(null);\n\n  // Mark which ends of a sideways-scrolling row have more beyond them.\n  useEffect(() => {\n    const el = row.current;\n    if (!el || loading) return;\n    const mark = () => {\n      const end = el.scrollWidth - el.clientWidth - Math.abs(el.scrollLeft);\n      el.toggleAttribute(\"data-more-start\", Math.abs(el.scrollLeft) > 2);\n      el.toggleAttribute(\"data-more-end\", end > 2);\n    };\n    mark();\n    el.addEventListener(\"scroll\", mark, { passive: true });\n    const ro = new ResizeObserver(mark);\n    ro.observe(el);\n    return () => {\n      el.removeEventListener(\"scroll\", mark);\n      ro.disconnect();\n    };\n  }, [loading, layout]);\n\n  const chips = layout === \"chips\";\n\n  return (\n    <SuggestionsContext.Provider value={{ layout, onPick, reduce, group, hovered, setHovered }}>\n      {/* Reduced motion: transforms jump to their end, the fade stays. The first frame is the same either way, so server and client agree. */}\n      <MotionConfig reducedMotion=\"user\">\n      <div role=\"group\" aria-label={label} aria-busy={loading || undefined} data-layout={layout} className={cn(\"@container relative min-w-0\", className)} {...rest}>\n        <AnimatePresence mode=\"popLayout\">\n          {loading ? (\n            <motion.ul\n              key=\"loading\"\n              aria-hidden\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0, transition: { duration: 0.12 } }}\n              className={cn(chips ? \"flex flex-wrap gap-2 @max-md:flex-nowrap @max-md:overflow-hidden\" : \"flex flex-col gap-1\")}\n            >\n              {(chips ? [148, 188, 120, 164] : [0, 1, 2]).map((w, i) => (\n                <li\n                  key={i}\n                  style={chips ? { width: w } : undefined}\n                  className={cn(\"shrink-0 animate-pulse-soft rounded-full bg-fg/[0.06]\", chips ? \"h-8\" : \"h-10 w-full rounded-lg\")}\n                />\n              ))}\n            </motion.ul>\n          ) : (\n            <motion.ul\n              key=\"ready\"\n              ref={row}\n              variants={container}\n              initial=\"hidden\"\n              animate=\"show\"\n              exit={{ opacity: 0, transition: { duration: 0.12 } }}\n              onPointerLeave={() => setHovered(null)}\n              className={cn(\n                chips\n                  ? cn(\n                      \"flex flex-wrap gap-2\",\n                      // Narrow: one row that scrolls sideways and snaps, with the edges faded where there's more.\n                      \"@max-md:flex-nowrap @max-md:snap-x @max-md:snap-mandatory @max-md:overflow-x-auto @max-md:overscroll-x-contain @max-md:[scrollbar-width:none]\",\n                      // Room inside the scroller for focus rings and shadows.\n                      \"@max-md:-m-1 @max-md:p-1 @max-md:scroll-px-1\",\n                      \"@max-md:data-more-end:[mask-image:linear-gradient(to_right,black_calc(100%-28px),transparent)]\",\n                      \"@max-md:data-more-start:[mask-image:linear-gradient(to_right,transparent,black_28px)]\",\n                      \"@max-md:data-more-start:data-more-end:[mask-image:linear-gradient(to_right,transparent,black_28px,black_calc(100%-28px),transparent)]\",\n                    )\n                  : \"relative flex flex-col\",\n              )}\n            >\n              {Children.toArray(children).map((child, i) => (\n                <IndexContext.Provider key={isValidElement(child) && child.key != null ? child.key : i} value={i}>\n                  {child}\n                </IndexContext.Provider>\n              ))}\n            </motion.ul>\n          )}\n        </AnimatePresence>\n      </div>\n      </MotionConfig>\n    </SuggestionsContext.Provider>\n  );\n}\n\nexport type SuggestionProps = Omit<React.ComponentProps<\"button\">, \"children\"> & {\n  /** What the suggestion says. */\n  children: React.ReactNode;\n  /** The full prompt handed to onPick. Defaults to the label when it's a string. */\n  prompt?: string;\n  /** A 16px icon before the label. */\n  icon?: React.ReactNode;\n};\n\nexport function Suggestion({ children, prompt, icon, className, onClick, ...rest }: SuggestionProps) {\n  const ctx = useContext(SuggestionsContext);\n  if (!ctx) throw new Error(\"Suggestion must be used inside Suggestions\");\n  const { layout, onPick, reduce, group, hovered, setHovered } = ctx;\n  const id = useId();\n  const index = useContext(IndexContext);\n  const full = prompt ?? (typeof children === \"string\" ? children : \"\");\n  const chips = layout === \"chips\";\n\n  // Arrive in order, 25ms apart; after the eighth nobody waits any longer.\n  const delay = 0.04 + Math.min(index, 7) * stagger.items * 1.25;\n  const item: Variants = {\n    hidden: { opacity: 0, y: 6, scale: 0.97, filter: \"blur(2px)\" },\n    show: { opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\", transition: { duration: reduce ? 0.16 : 0.32, ease: ease.out, delay } },\n  };\n\n  return (\n    <motion.li variants={item} className={cn(\"relative min-w-0\", chips ? \"shrink-0 @max-md:snap-start\" : \"\")}>\n      {!chips && (\n        <AnimatePresence>\n          {hovered === id && (\n            // One highlight for the whole list, gliding to whichever row is under the pointer.\n            <motion.span\n              layoutId={`${group}-highlight`}\n              aria-hidden\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0, transition: { duration: 0.15 } }}\n              transition={reduce ? { duration: 0 } : spring.follow}\n              className=\"pointer-events-none absolute inset-0 rounded-lg bg-hover\"\n            />\n          )}\n        </AnimatePresence>\n      )}\n      <button\n        type=\"button\"\n        onPointerEnter={(e) => e.pointerType === \"mouse\" && setHovered(id)}\n        onClick={(e) => {\n          onClick?.(e);\n          if (!e.defaultPrevented && full) onPick?.(full);\n        }}\n        className={cn(\n          \"group/suggestion relative flex min-w-0 select-none items-center outline-none\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          chips\n            ? cn(\n                \"h-8 max-w-[18rem] gap-1.5 rounded-full border border-line-2 bg-raised px-3 text-[12.5px] text-fg-2 shadow-[var(--shadow)]\",\n                \"transition-[background-color,border-color,color,scale] duration-150 ease-out hover:border-fg-4 hover:bg-hover hover:text-fg active:scale-[0.96] active:duration-75\",\n                // 32px drawn, 44px to a finger.\n                \"before:absolute before:-inset-y-1.5 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n                icon ? \"ps-2.5\" : undefined,\n              )\n            : cn(\n                \"h-10 w-full gap-2.5 rounded-lg px-3 text-left text-[13px] text-fg-2\",\n                \"transition-[color,scale] duration-150 ease-out hover:text-fg active:scale-[0.99] active:duration-75 pointer-coarse:active:bg-hover\",\n              ),\n          className,\n        )}\n        {...rest}\n      >\n        {icon && (\n          <span aria-hidden className=\"grid size-4 shrink-0 place-items-center text-fg-3 transition-colors duration-150 group-hover/suggestion:text-fg-2 [&_svg]:size-4\">\n            {icon}\n          </span>\n        )}\n        <span className=\"min-w-0 truncate\">{children}</span>\n        {!chips && (\n          <ArrowRight\n            size={14}\n            className=\"ms-auto shrink-0 text-fg-4 transition-[translate,color] duration-150 ease-out group-hover/suggestion:translate-x-0.5 group-hover/suggestion:text-fg-3 motion-reduce:transition-none\"\n          />\n        )}\n      </button>\n    </motion.li>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/suggestions"
}
