{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "suggestion-chips",
  "title": "Suggestion Chips",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Starter prompts for an empty chat or follow-ups after a reply, as wrapping pills, a swipeable row with faded edges, or cards with descriptions, plus loading and refresh states.",
  "dependencies": [
    "motion@^12",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/suggestion-chips.tsx",
      "content": "// Ballmac UI: Suggestion Chips. https://ui.ballmac.com/components/suggestion-chips\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowUpRight, RefreshCw } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype Suggestion = {\n  /** Text on the chip. */\n  label: string\n  /** Text sent when chosen. Defaults to `label`. */\n  prompt?: string\n  /** Second line, used by the \"cards\" layout. */\n  description?: string\n  /** Leading icon. */\n  icon?: React.ReactNode\n}\n\ntype SuggestionChipsProps = Omit<React.ComponentProps<\"div\">, \"onSelect\"> & {\n  /** The prompts to offer. */\n  suggestions: Suggestion[]\n  /** Called with the prompt text and the suggestion that was chosen. */\n  onSelect?: (prompt: string, suggestion: Suggestion) => void\n  /** \"pills\" wraps onto several lines, \"scroll\" keeps one swipeable row with faded edges, \"cards\" is a grid with descriptions. */\n  variant?: \"pills\" | \"scroll\" | \"cards\"\n  /** Accessible name of the group. */\n  label?: string\n  /** When set, a refresh button appears after the chips. */\n  onRefresh?: () => void\n  /** Accessible name of the refresh button. */\n  refreshLabel?: string\n  /** Disables every chip, for example while a reply is streaming. */\n  disabled?: boolean\n  /** Shows placeholder chips while suggestions load. */\n  loading?: boolean\n}\n\nconst chipBase =\n  \"inline-flex shrink-0 items-center gap-2 rounded-full border bg-background text-sm font-medium text-foreground shadow-xs outline-none transition-[color,background-color,border-color,box-shadow,transform] duration-150 select-none hover:border-foreground/25 hover:bg-accent focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground\"\n\nfunction SuggestionChips({\n  suggestions,\n  onSelect,\n  variant = \"pills\",\n  label,\n  onRefresh,\n  refreshLabel,\n  disabled = false,\n  loading = false,\n  className,\n  ...props\n}: SuggestionChipsProps) {\n  const msg = useMessages()\n  label ??= msg(\"suggestion-chips.label\", \"Suggested prompts\")\n  refreshLabel ??= msg(\"suggestion-chips.refreshLabel\", \"Show other suggestions\")\n  const reduce = useReducedMotion()\n  const [spins, setSpins] = React.useState(0)\n\n  function choose(s: Suggestion) {\n    onSelect?.(s.prompt ?? s.label, s)\n  }\n\n  const rootClass =\n    variant === \"cards\"\n      ? \"grid w-full gap-2 sm:grid-cols-2\"\n      : variant === \"scroll\"\n        ? \"-mx-1 flex w-full snap-x gap-2 overflow-x-auto px-1 py-1 [-ms-overflow-style:none] [scrollbar-width:none] [mask-image:linear-gradient(to_right,transparent,black_1.25rem,black_calc(100%-1.25rem),transparent)] [&::-webkit-scrollbar]:hidden\"\n        : \"flex w-full flex-wrap gap-2\"\n\n  const item = (i: number) => ({\n    initial: reduce ? { opacity: 0 } : { opacity: 0, y: 8 },\n    animate: { opacity: 1, y: 0 },\n    transition: { duration: 0.28, delay: reduce ? 0 : Math.min(i, 8) * 0.04, ease: [0.22, 1, 0.36, 1] as const },\n  })\n\n  return (\n    <div\n      data-slot=\"suggestion-chips\"\n      data-variant={variant}\n      role=\"group\"\n      aria-label={label}\n      aria-busy={loading || undefined}\n      className={cn(rootClass, className)}\n      {...props}\n    >\n      {loading\n        ? Array.from({ length: 4 }, (_, i) => (\n            <span\n              key={i}\n              aria-hidden=\"true\"\n              className={cn(\n                \"animate-pulse bg-muted motion-reduce:animate-none\",\n                variant === \"cards\" ? \"h-16 rounded-xl\" : \"h-9 shrink-0 rounded-full\",\n                variant !== \"cards\" && [\"w-32\", \"w-44\", \"w-28\", \"w-40\"][i]\n              )}\n            />\n          ))\n        : suggestions.map((s, i) =>\n            variant === \"cards\" ? (\n              <motion.button\n                key={s.label}\n                type=\"button\"\n                disabled={disabled}\n                onClick={() => choose(s)}\n                {...item(i)}\n                className=\"group/chip flex min-w-0 items-start gap-3 rounded-xl border bg-card p-3.5 text-start shadow-xs outline-none transition-[border-color,background-color,box-shadow] duration-150 hover:border-foreground/25 hover:bg-accent/60 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none\"\n              >\n                {s.icon && (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border bg-muted text-foreground [&_svg]:size-4\"\n                  >\n                    {s.icon}\n                  </span>\n                )}\n                <span className=\"grid min-w-0 flex-1 gap-0.5\">\n                  <span className=\"text-sm font-medium text-foreground\">{s.label}</span>\n                  {s.description && (\n                    <span className=\"line-clamp-2 text-[13px] leading-5 text-muted-foreground\">{s.description}</span>\n                  )}\n                </span>\n                <ArrowUpRight\n                  aria-hidden=\"true\"\n                  className=\"mt-1 size-4 shrink-0 text-muted-foreground opacity-0 transition-[opacity,transform] duration-150 group-hover/chip:translate-x-0.5 group-hover/chip:-translate-y-0.5 group-hover/chip:opacity-100 group-focus-visible/chip:opacity-100 motion-reduce:transition-none rtl:-scale-x-100\"\n                />\n              </motion.button>\n            ) : (\n              <motion.button\n                key={s.label}\n                type=\"button\"\n                disabled={disabled}\n                onClick={() => choose(s)}\n                {...item(i)}\n                className={cn(chipBase, \"h-9 px-3.5\", variant === \"scroll\" && \"snap-start\", s.icon && \"ps-3\")}\n              >\n                {s.icon}\n                <span className=\"whitespace-nowrap\">{s.label}</span>\n              </motion.button>\n            )\n        )}\n      {onRefresh && !loading && (\n        <button\n          type=\"button\"\n          disabled={disabled}\n          aria-label={refreshLabel}\n          title={refreshLabel}\n          onClick={() => {\n            setSpins((n) => n + 1)\n            onRefresh()\n          }}\n          className={cn(\n            \"inline-flex size-9 shrink-0 items-center justify-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n            variant === \"cards\" && \"justify-self-start sm:col-span-2\"\n          )}\n        >\n          <motion.span\n            aria-hidden=\"true\"\n            animate={{ rotate: reduce ? 0 : spins * 180 }}\n            transition={{ type: \"spring\", stiffness: 240, damping: 20 }}\n            className=\"flex\"\n          >\n            <RefreshCw className=\"size-4\" />\n          </motion.span>\n        </button>\n      )}\n    </div>\n  )\n}\n\nexport { SuggestionChips, type SuggestionChipsProps, type Suggestion }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/suggestion-chips.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "chat",
      "prompts",
      "suggestions",
      "chips"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "suggestions is an array of { label, prompt?, description?, icon? }. onSelect receives the prompt text. variant is pills | scroll | cards. loading shows placeholders and onRefresh adds a shuffle button.",
      "whenToUse": [
        "Empty state of a chat with example questions",
        "Follow-up questions under an answer"
      ],
      "whenNotToUse": [
        "Filtering a list (use filter-chips or tabs)",
        "Free-form input; use prompt-input"
      ],
      "composesWith": [
        "prompt-input",
        "ai-chat",
        "ai-message"
      ],
      "a11y": [
        {
          "keys": "Tab / Shift+Tab",
          "action": "Moves between chips"
        },
        {
          "keys": "Enter / Space",
          "action": "Sends the chosen prompt"
        },
        {
          "keys": "Reduced motion",
          "action": "Chips fade in without sliding"
        }
      ],
      "customization": [
        "variant: pills | scroll | cards",
        "loading",
        "disabled",
        "onRefresh and refreshLabel"
      ]
    },
    "examples": [
      "suggestion-chips-demo",
      "suggestion-chips-cards"
    ],
    "url": "https://ui.ballmac.com/components/suggestion-chips"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}