{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-motion",
  "title": "Motion command",
  "description": "Styled command: the Motion set. 5 decorative command variations (SlideCommand, PopCommand, FadeCommand, SpringCommand, MorphCommand) 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-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport {\n  Calculator,\n  Calendar,\n  CreditCard,\n  FileText,\n  Search,\n  Settings,\n  Smile,\n  User,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion command family: 5 SELF-CONTAINED command palettes. NO cmdk, NO radix, NO\n   portal. They all share the same skeleton (a search input + a filtered list) and\n   differ ONLY in the enter/exit motion of the rows: slide, pop, fade, spring,\n   morph. The list filters as the user types; AnimatePresence matching removes the\n   row that no longer matches and animates the new one in. The arrow keys move the\n   highlight, Enter selects, Escape clears. Color comes ONLY from tokens, via alpha\n   color-mix. No transform under reduced motion, only a short fade. */\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/* 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 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 emptyBase = \"py-6 text-center text-sm text-muted-foreground\"\n\n/* Bir hareket lezzeti: giris/cikis transformu + zamanlama. */\ninterface Flavor {\n  initial: Record<string, number>\n  animate: Record<string, number>\n  exit: Record<string, number>\n  duration: number\n  stagger: number\n  spring?: { stiffness: number; damping: number }\n  layout?: boolean\n}\n\n/* Row transition. A short fade under reduced motion, no transform. */\nfunction rowTransition(f: Flavor, index: number, reduce: boolean) {\n  if (reduce) return { duration: 0.12, ease: \"easeOut\" as const }\n  const delay = Math.min(index * f.stagger, 0.12)\n  if (f.spring) {\n    return {\n      type: \"spring\" as const,\n      stiffness: f.spring.stiffness,\n      damping: f.spring.damping,\n      delay,\n    }\n  }\n  return { duration: f.duration, ease: \"easeOut\" as const, delay }\n}\n\nconst fadeOnly = { opacity: 0 }\n\n/* Shared shell: every flavour uses this, only Flavor changes. */\nfunction MotionPalette({ props, flavor }: { props: CommandProps; flavor: Flavor }) {\n  const {\n    className,\n    size = \"md\",\n    placeholder = \"Type a command...\",\n    items = defaultItems,\n  } = props\n  const p = usePalette(items)\n  const reduce = useReducedMotion() ?? false\n\n  return (\n    <div\n      data-slot=\"styled-command\"\n      className={cn(rootBase, paletteWidth[size], className)}\n    >\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={\n            p.filtered.length > 0 ? p.optionId(p.active) : undefined\n          }\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      <div id={p.listId} role=\"listbox\" aria-label=\"Command palette\" className={listBase}>\n        <AnimatePresence initial={false}>\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              layout={flavor.layout && !reduce ? true : undefined}\n              initial={reduce ? fadeOnly : flavor.initial}\n              animate={reduce ? { opacity: 1 } : flavor.animate}\n              exit={reduce ? fadeOnly : flavor.exit}\n              transition={rowTransition(flavor, i, reduce)}\n              onMouseEnter={() => p.setActive(i)}\n              onClick={() => p.select(i)}\n              className={cn(\n                optionBase,\n                \"transition-colors\",\n                i === p.active && \"bg-accent text-accent-foreground\"\n              )}\n            >\n              {item.icon}\n              <span className=\"flex-1 truncate\">{item.label}</span>\n            </motion.div>\n          ))}\n        </AnimatePresence>\n        {p.filtered.length === 0 ? <p className={emptyBase}>No results found.</p> : null}\n      </div>\n    </div>\n  )\n}\n\n/* SlideCommand: satirlar soldan kayarak girer, saga kayarak cikar. */\nexport function SlideCommand(props: CommandProps) {\n  return (\n    <MotionPalette\n      props={props}\n      flavor={{\n        initial: { opacity: 0, x: -12 },\n        animate: { opacity: 1, x: 0 },\n        exit: { opacity: 0, x: 12 },\n        duration: 0.18,\n        stagger: 0.02,\n      }}\n    />\n  )\n}\n\n/* PopCommand: satirlar kucukten buyuyerek patlar. */\nexport function PopCommand(props: CommandProps) {\n  return (\n    <MotionPalette\n      props={props}\n      flavor={{\n        initial: { opacity: 0, scale: 0.9 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.9 },\n        duration: 0.16,\n        stagger: 0.015,\n      }}\n    />\n  )\n}\n\n/* FadeCommand: opacity only; the calmest flavor. */\nexport function FadeCommand(props: CommandProps) {\n  return (\n    <MotionPalette\n      props={props}\n      flavor={{\n        initial: { opacity: 0 },\n        animate: { opacity: 1 },\n        exit: { opacity: 0 },\n        duration: 0.22,\n        stagger: 0.03,\n      }}\n    />\n  )\n}\n\n/* SpringCommand: yay zamanlamasi ile asagidan gelir. */\nexport function SpringCommand(props: CommandProps) {\n  return (\n    <MotionPalette\n      props={props}\n      flavor={{\n        initial: { opacity: 0, y: 10 },\n        animate: { opacity: 1, y: 0 },\n        exit: { opacity: 0, y: -10 },\n        duration: 0.2,\n        stagger: 0.02,\n        spring: { stiffness: 420, damping: 30 },\n      }}\n    />\n  )\n}\n\n/* MorphCommand: layout animasyonu ile kalan satirlar bosluga akar. */\nexport function MorphCommand(props: CommandProps) {\n  return (\n    <MotionPalette\n      props={props}\n      flavor={{\n        initial: { opacity: 0, scaleX: 0.94, y: 8 },\n        animate: { opacity: 1, scaleX: 1, y: 0 },\n        exit: { opacity: 0, scaleX: 0.94, y: -8 },\n        duration: 0.2,\n        stagger: 0.02,\n        layout: true,\n      }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/command-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "command"
  ],
  "type": "registry:component"
}