{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "empty-results",
  "title": "Empty Results",
  "author": "ecomcn <hello@ecomcn.dev>",
  "description": "A no-match state that names the filters that emptied the grid, offers the single change that brings the most back, and lets shoppers remove filters one at a time. Left-aligned, never centred.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/ecomcn/empty-results/empty-results.tsx",
      "content": "import * as React from \"react\"\nimport { ArrowRight, X } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface EmptyResultsFilter {\n  /** Stable key, e.g. `color:sage`. */\n  id: string\n  /** What the shopper sees — \"Sage\", \"Up to $80\", \"In stock only\". */\n  label: string\n  onRemove?: () => void\n}\n\nexport interface EmptyResultsSuggestion {\n  /** The one change that helps most, as an action: \"Remove “Up to $80”\". */\n  label: string\n  /** How many results that change brings back. */\n  count?: number\n  onApply: () => void\n}\n\nexport interface EmptyResultsProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  /** The search term, when there is one. */\n  query?: string\n  /** The filters that are on. Named in the headline and removable one by one. */\n  filters?: EmptyResultsFilter[]\n  suggestion?: EmptyResultsSuggestion\n  onClearAll?: () => void\n  /** Overrides the generated headline. */\n  title?: React.ReactNode\n  description?: React.ReactNode\n  locale?: string\n  noun?: { one: string; other: string }\n}\n\n/**\n * \"No results\" tells a shopper nothing. This names what emptied the grid and\n * offers the single change that brings the most back — the difference\n * between a dead end and a detour.\n */\nexport function EmptyResults({\n  query,\n  filters = [],\n  suggestion,\n  onClearAll,\n  title,\n  description,\n  locale,\n  noun = { one: \"product\", other: \"products\" },\n  className,\n  children,\n  ...props\n}: EmptyResultsProps) {\n  // Quoted, because filter labels are tokens (\"Up to $80\", \"In stock only\")\n  // and read badly as bare words in the middle of a sentence.\n  const list = new Intl.ListFormat(locale, { type: \"conjunction\" }).format(\n    filters.map((f) => `“${f.label}”`)\n  )\n  const headline =\n    title ??\n    (query && filters.length\n      ? `Nothing for “${query}” in ${list}.`\n      : query\n        ? `Nothing for “${query}”.`\n        : filters.length\n          ? `Nothing matches ${list}.`\n          : \"Nothing here yet.\")\n\n  const counted = (n: number) =>\n    `${new Intl.NumberFormat(locale).format(n)} ${\n      new Intl.PluralRules(locale).select(n) === \"one\" ? noun.one : noun.other\n    }`\n\n  return (\n    // Left-aligned on purpose. A centred icon-and-sentence empty state is the\n    // most reliable tell of a generated interface.\n    <div\n      className={cn(\"flex flex-col items-start gap-5 py-10 sm:py-14\", className)}\n      {...props}\n    >\n      <p className=\"text-xs font-medium tracking-[0.14em] text-muted-foreground uppercase\">\n        No results\n      </p>\n      <h2 className=\"max-w-2xl text-2xl leading-tight text-balance sm:text-3xl\">\n        {headline}\n      </h2>\n      {description ? (\n        <p className=\"max-w-lg text-sm leading-relaxed text-muted-foreground\">\n          {description}\n        </p>\n      ) : null}\n\n      {suggestion ? (\n        <Button type=\"button\" onClick={suggestion.onApply} className=\"h-10 gap-2 text-[13px]\">\n          {suggestion.label}\n          {suggestion.count !== undefined && (\n            <span className=\"tabular-nums opacity-70\">· {counted(suggestion.count)}</span>\n          )}\n          <ArrowRight className=\"size-4\" aria-hidden />\n        </Button>\n      ) : null}\n\n      {filters.some((f) => f.onRemove) || onClearAll ? (\n        <div className=\"flex flex-wrap items-center gap-1.5 text-sm\">\n          <span className=\"mr-1 text-muted-foreground\">\n            {suggestion ? \"Or remove one:\" : \"Remove one:\"}\n          </span>\n          {filters.map((filter) =>\n            filter.onRemove ? (\n              <button\n                key={filter.id}\n                type=\"button\"\n                onClick={filter.onRemove}\n                className=\"group inline-flex h-7 items-center gap-1.5 rounded-sm border bg-secondary/60 pr-1.5 pl-2 text-xs transition-colors outline-none hover:border-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <span className=\"sr-only\">Remove filter:</span>\n                {filter.label}\n                <X className=\"size-3 text-muted-foreground group-hover:text-foreground\" aria-hidden />\n              </button>\n            ) : null\n          )}\n          {onClearAll ? (\n            <button\n              type=\"button\"\n              onClick={onClearAll}\n              className=\"ml-1 text-xs underline underline-offset-4 outline-none hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              Clear all\n            </button>\n          ) : null}\n        </div>\n      ) : null}\n\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ecomcn/empty-results.tsx"
    }
  ],
  "categories": [
    "ecommerce",
    "catalog",
    "listing",
    "empty-state"
  ],
  "type": "registry:component"
}