{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-empty",
  "title": "Empty command",
  "description": "Styled command: the Empty set. 5 decorative command variations (EmptyCommand, LoadingCommand, ErrorCommand, RecentCommand, SuggestCommand) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/command-empty.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport {\n  Calculator,\n  Calendar,\n  Clock,\n  CreditCard,\n  FileText,\n  Lightbulb,\n  RotateCcw,\n  Search,\n  SearchX,\n  Settings,\n  Smile,\n  TriangleAlert,\n  User,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Empty command family: 5 SELF-CONTAINED command palettes, all focused on the\n   states OFF the happy path. NO cmdk, NO radix, NO portal. Every state is\n   reachable by typing into the real input: a query with no match opens the\n   empty/error/suggest state, every keystroke starts the loading phase, and an\n   empty query shows the recent searches. The loading duration is a FIXED number\n   (no Date.now/Math.random) and is cancelled in the effect cleanup. Color comes\n   ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst paletteWidth: Record<StyledSize, string> = {\n  sm: \"w-64\",\n  md: \"w-80\",\n  lg: \"w-96\",\n  xl: \"w-[28rem]\",\n}\n\nexport interface CommandItem {\n  label: React.ReactNode\n  group?: string\n  icon?: React.ReactNode\n  onSelect?: () => void\n}\n\ninterface CommandProps {\n  className?: string\n  size?: StyledSize\n  placeholder?: string\n  items?: CommandItem[]\n}\n\n/* The label can be a ReactNode; flatten it to plain text for filtering. */\nfunction nodeText(node: React.ReactNode): string {\n  if (node === null || node === undefined || typeof node === \"boolean\") return \"\"\n  if (typeof node === \"string\" || typeof node === \"number\") return String(node)\n  if (Array.isArray(node)) return node.map(nodeText).join(\"\")\n  if (React.isValidElement(node)) {\n    return nodeText((node.props as { children?: React.ReactNode }).children)\n  }\n  return \"\"\n}\n\n/* Varsayilan liste: prop'suz render eder. */\nconst defaultItems: CommandItem[] = [\n  { label: \"Calendar\", group: \"Suggestions\", icon: <Calendar /> },\n  { label: \"Search Emoji\", group: \"Suggestions\", icon: <Smile /> },\n  { label: \"Calculator\", group: \"Suggestions\", icon: <Calculator /> },\n  { label: \"Profile\", group: \"Settings\", icon: <User /> },\n  { label: \"Billing\", group: \"Settings\", icon: <CreditCard /> },\n  { label: \"Settings\", group: \"Settings\", icon: <Settings /> },\n  { label: \"New Document\", group: \"Actions\", icon: <FileText /> },\n]\n\n/* Sabitler: hepsi deterministik. */\nconst LOADING_MS = 420\nconst SKELETON_ROWS = 4\nconst recentQueries = [\"calendar\", \"billing\", \"settings\"]\n\n/* Shared state plus combobox/listbox ids. useId keeps several instances on\n   the same page from colliding. */\nfunction usePalette(source: CommandItem[]) {\n  const uid = React.useId()\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n\n  const filtered = React.useMemo(() => {\n    const q = query.trim().toLowerCase()\n    if (q.length === 0) return source\n    return source.filter((item) => nodeText(item.label).toLowerCase().includes(q))\n  }, [query, source])\n\n  React.useEffect(() => {\n    setActive((prev) => (prev >= filtered.length ? 0 : prev))\n  }, [filtered.length])\n\n  const select = React.useCallback(\n    (index: number) => {\n      const item = filtered[index]\n      if (item) item.onSelect?.()\n    },\n    [filtered]\n  )\n\n  const onKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        setActive((prev) => (filtered.length === 0 ? 0 : (prev + 1) % filtered.length))\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        setActive((prev) =>\n          filtered.length === 0 ? 0 : (prev - 1 + filtered.length) % filtered.length\n        )\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        select(active)\n      } else if (e.key === \"Escape\") {\n        e.preventDefault()\n        setQuery(\"\")\n        setActive(0)\n      }\n    },\n    [filtered.length, active, select]\n  )\n\n  const listId = `${uid}-list`\n  const optionId = React.useCallback((i: number) => `${uid}-option-${i}`, [uid])\n\n  return { query, setQuery, active, setActive, filtered, select, onKeyDown, listId, optionId }\n}\n\nconst rootBase =\n  \"flex flex-col overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none\"\n\nconst inputRowBase = \"flex items-center gap-2 border-b border-border px-3\"\n\nconst inputBase =\n  \"h-11 w-full rounded-md bg-transparent py-3 text-sm text-popover-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\nconst listBase = \"max-h-72 overflow-y-auto p-1.5\"\n\nconst optionBase =\n  \"flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground [&_i]:text-base [&_i]:leading-none [&_i]:text-muted-foreground\"\n\nconst stateWrap = \"flex flex-col items-center gap-2 px-6 py-8 text-center\"\n\nconst stateTitle = \"text-sm font-medium text-foreground\"\n\nconst stateBody = \"text-xs text-muted-foreground\"\n\nconst chipBtn =\n  \"inline-flex h-7 select-none items-center gap-1.5 rounded-full border border-border bg-[color-mix(in_oklab,var(--color-foreground)_5%,transparent)] px-2.5 text-xs text-muted-foreground outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-3 [&_svg]:shrink-0 [&_i]:text-xs [&_i]:leading-none\"\n\n/* Search row: every palette uses the same header. */\nfunction SearchRow({\n  p,\n  placeholder,\n}: {\n  p: ReturnType<typeof usePalette>\n  placeholder: string\n}) {\n  return (\n    <div className={inputRowBase}>\n      <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n      <input\n        role=\"combobox\"\n        aria-expanded={true}\n        aria-controls={p.listId}\n        aria-autocomplete=\"list\"\n        aria-activedescendant={p.filtered.length > 0 ? p.optionId(p.active) : undefined}\n        value={p.query}\n        placeholder={placeholder}\n        onChange={(e) => p.setQuery(e.target.value)}\n        onKeyDown={p.onKeyDown}\n        className={inputBase}\n      />\n    </div>\n  )\n}\n\n/* Filtrelenmis satirlar: tum paletlerde ortak. */\nfunction Rows({ p, reduce }: { p: ReturnType<typeof usePalette>; reduce: boolean }) {\n  return (\n    <>\n      {p.filtered.map((item, i) => (\n        <motion.div\n          key={nodeText(item.label) || String(i)}\n          id={p.optionId(i)}\n          role=\"option\"\n          aria-selected={i === p.active}\n          tabIndex={-1}\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={reduce ? { duration: 0 } : { duration: 0.15, ease: \"easeOut\" }}\n          onMouseEnter={() => p.setActive(i)}\n          onClick={() => p.select(i)}\n          className={cn(optionBase, i === p.active && \"bg-accent text-accent-foreground\")}\n        >\n          {item.icon}\n          <span className=\"flex-1 truncate\">{item.label}</span>\n        </motion.div>\n      ))}\n    </>\n  )\n}\n\n/* EmptyCommand: eslesme yoksa acikli bir bos durum. Yazarak deneyin. */\nexport function EmptyCommand({\n  className,\n  size = \"md\",\n  placeholder = \"Type a command...\",\n  items = defaultItems,\n}: CommandProps) {\n  const p = usePalette(items)\n  const reduce = useReducedMotion() ?? false\n\n  return (\n    <div data-slot=\"styled-command\" className={cn(rootBase, paletteWidth[size], className)}>\n      <SearchRow p={p} placeholder={placeholder} />\n      <div id={p.listId} role=\"listbox\" aria-label=\"Command palette\" className={listBase}>\n        {p.filtered.length === 0 ? (\n          <div className={stateWrap}>\n            <SearchX className=\"size-6 text-muted-foreground\" />\n            <p className={stateTitle}>No results found</p>\n            <p className={stateBody}>\n              Nothing matches that query. Try a shorter word or clear the search.\n            </p>\n            <button type=\"button\" className={chipBtn} onClick={() => p.setQuery(\"\")}>\n              <RotateCcw />\n              Clear search\n            </button>\n          </div>\n        ) : (\n          <Rows p={p} reduce={reduce} />\n        )}\n      </div>\n    </div>\n  )\n}\n\n/* LoadingCommand: every keystroke opens a loading phase of FIXED duration; skeleton\n   rows are shown. The timeout is cancelled in the effect cleanup. */\nexport function LoadingCommand({\n  className,\n  size = \"md\",\n  placeholder = \"Type a command...\",\n  items = defaultItems,\n}: CommandProps) {\n  const p = usePalette(items)\n  const reduce = useReducedMotion() ?? false\n  const [loading, setLoading] = React.useState(false)\n\n  React.useEffect(() => {\n    setLoading(true)\n    const id = window.setTimeout(() => setLoading(false), LOADING_MS)\n    return () => window.clearTimeout(id)\n  }, [p.query])\n\n  return (\n    <div data-slot=\"styled-command\" className={cn(rootBase, paletteWidth[size], className)}>\n      <SearchRow p={p} placeholder={placeholder} />\n      <div\n        id={p.listId}\n        role=\"listbox\"\n        aria-label=\"Command palette\"\n        aria-busy={loading}\n        className={listBase}\n      >\n        {loading ? (\n          <div className=\"flex flex-col gap-1 p-1\">\n            {Array.from({ length: SKELETON_ROWS }).map((_, i) => (\n              <div key={i} className=\"flex items-center gap-2 rounded-lg px-1.5 py-2\">\n                <span className=\"size-4 shrink-0 animate-pulse rounded bg-[color-mix(in_oklab,var(--color-foreground)_12%,transparent)] motion-reduce:animate-none\" />\n                <span\n                  className={cn(\n                    \"h-3 animate-pulse rounded bg-[color-mix(in_oklab,var(--color-foreground)_12%,transparent)] motion-reduce:animate-none\",\n                    i % 2 === 0 ? \"w-2/3\" : \"w-1/2\"\n                  )}\n                />\n              </div>\n            ))}\n          </div>\n        ) : p.filtered.length === 0 ? (\n          <p className=\"py-6 text-center text-sm text-muted-foreground\">No results found.</p>\n        ) : (\n          <Rows p={p} reduce={reduce} />\n        )}\n      </div>\n    </div>\n  )\n}\n\n/* ErrorCommand: a non-matching query represents a failed search; it shows a\n   danger-toned error card and a retry action. */\nexport function ErrorCommand({\n  className,\n  size = \"md\",\n  placeholder = \"Type a command...\",\n  items = defaultItems,\n}: CommandProps) {\n  const p = usePalette(items)\n  const reduce = useReducedMotion() ?? false\n  const failed = p.query.trim().length > 0 && p.filtered.length === 0\n\n  return (\n    <div data-slot=\"styled-command\" className={cn(rootBase, paletteWidth[size], className)}>\n      <SearchRow p={p} placeholder={placeholder} />\n      <div id={p.listId} role=\"listbox\" aria-label=\"Command palette\" className={listBase}>\n        {failed ? (\n          <motion.div\n            key=\"error\"\n            role=\"alert\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            transition={reduce ? { duration: 0 } : { duration: 0.18, ease: \"easeOut\" }}\n            className={cn(\n              stateWrap,\n              \"m-1 rounded-lg border border-[color-mix(in_oklab,var(--color-danger)_45%,transparent)] bg-[color-mix(in_oklab,var(--color-danger)_10%,transparent)]\"\n            )}\n          >\n            <TriangleAlert className=\"size-6 text-danger\" />\n            <p className={stateTitle}>Search failed</p>\n            <p className={stateBody}>\n              The command index could not answer that query. Retry or search for something else.\n            </p>\n            <button type=\"button\" className={chipBtn} onClick={() => p.setQuery(\"\")}>\n              <RotateCcw />\n              Retry\n            </button>\n          </motion.div>\n        ) : (\n          <Rows p={p} reduce={reduce} />\n        )}\n      </div>\n    </div>\n  )\n}\n\n/* RecentCommand: sorgu bosken son aramalar; yazinca normal sonuclar. */\nexport function RecentCommand({\n  className,\n  size = \"md\",\n  placeholder = \"Type a command...\",\n  items = defaultItems,\n}: CommandProps) {\n  const p = usePalette(items)\n  const reduce = useReducedMotion() ?? false\n  const blank = p.query.trim().length === 0\n\n  return (\n    <div data-slot=\"styled-command\" className={cn(rootBase, paletteWidth[size], className)}>\n      <SearchRow p={p} placeholder={placeholder} />\n      <div id={p.listId} role=\"listbox\" aria-label=\"Command palette\" className={listBase}>\n        {blank ? (\n          <div className=\"p-1\">\n            <p className=\"px-1.5 py-1.5 text-xs font-medium text-muted-foreground\">\n              Recent searches\n            </p>\n            <div className=\"flex flex-col gap-1\">\n              {recentQueries.map((q) => (\n                <button\n                  key={q}\n                  type=\"button\"\n                  className={cn(optionBase, \"w-full text-left hover:bg-accent\")}\n                  onClick={() => p.setQuery(q)}\n                >\n                  <Clock />\n                  <span className=\"flex-1 truncate\">{q}</span>\n                </button>\n              ))}\n            </div>\n          </div>\n        ) : p.filtered.length === 0 ? (\n          <p className=\"py-6 text-center text-sm text-muted-foreground\">No results found.</p>\n        ) : (\n          <Rows p={p} reduce={reduce} />\n        )}\n      </div>\n    </div>\n  )\n}\n\n/* SuggestCommand: hand-picked suggestions when nothing matches. */\nexport function SuggestCommand({\n  className,\n  size = \"md\",\n  placeholder = \"Type a command...\",\n  items = defaultItems,\n}: CommandProps) {\n  const p = usePalette(items)\n  const reduce = useReducedMotion() ?? false\n  const suggestions = React.useMemo(() => items.slice(0, 3), [items])\n\n  return (\n    <div data-slot=\"styled-command\" className={cn(rootBase, paletteWidth[size], className)}>\n      <SearchRow p={p} placeholder={placeholder} />\n      <div id={p.listId} role=\"listbox\" aria-label=\"Command palette\" className={listBase}>\n        {p.filtered.length === 0 ? (\n          <div className={stateWrap}>\n            <Lightbulb className=\"size-6 text-muted-foreground\" />\n            <p className={stateTitle}>No direct match</p>\n            <p className={stateBody}>Here is what people usually look for instead.</p>\n            <div className=\"flex flex-wrap justify-center gap-1.5 pt-1\">\n              {suggestions.map((item, i) => (\n                <button\n                  key={nodeText(item.label) || String(i)}\n                  type=\"button\"\n                  className={chipBtn}\n                  onClick={() => p.setQuery(nodeText(item.label))}\n                >\n                  {item.label}\n                </button>\n              ))}\n            </div>\n          </div>\n        ) : (\n          <Rows p={p} reduce={reduce} />\n        )}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/command-empty.tsx"
    }
  ],
  "categories": [
    "styled",
    "command"
  ],
  "type": "registry:component"
}