{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spotlight-search",
  "title": "Spotlight Search",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A macOS Spotlight-style command palette on cmdk: a large translucent search field, grouped results with app-icon tiles, fuzzy filtering and arrow-key navigation, inline or as a ⌘K dialog.",
  "dependencies": [
    "cmdk@^1",
    "radix-ui",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/spotlight-search.tsx",
      "content": "// Ballmac UI: Spotlight Search. https://ui.ballmac.com/components/spotlight-search\n\"use client\"\n\nimport * as React from \"react\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { SearchIcon } from \"lucide-react\"\nimport { Dialog as DialogPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype SpotlightSearchProps = React.ComponentProps<typeof CommandPrimitive>\n\n/**\n * cmdk scrolls the selected row (and its group heading) into view with scrollIntoView, which also\n * scrolls the page when the palette is off-screen, e.g. an inline demo below the fold. Route those\n * calls to the results list only, so selection never moves the page.\n */\nfunction scrollWithinList(this: HTMLElement) {\n  const list = this.closest<HTMLElement>(\"[cmdk-list]\")\n  if (!list) return\n  const bounds = list.getBoundingClientRect()\n  const rect = this.getBoundingClientRect()\n  if (rect.top < bounds.top) list.scrollTop -= bounds.top - rect.top + 8\n  else if (rect.bottom > bounds.bottom) list.scrollTop += rect.bottom - bounds.bottom + 8\n}\n\nfunction keepScrollInList(el: HTMLElement | null) {\n  if (el) el.scrollIntoView = scrollWithinList\n}\n\nfunction setRef<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (typeof ref === \"function\") ref(value)\n  else if (ref) (ref as React.RefObject<T | null>).current = value\n}\n\n/**\n * A macOS Spotlight-style command palette on cmdk: a large translucent search field over grouped results.\n * Renders inline; wrap it in SpotlightSearchDialog for a ⌘K overlay.\n */\nfunction SpotlightSearch({ className, loop = true, ...props }: SpotlightSearchProps) {\n  return (\n    <CommandPrimitive\n      data-slot=\"spotlight-search\"\n      loop={loop}\n      className={cn(\n        \"flex w-full flex-col overflow-hidden rounded-[22px] border border-white/45 bg-background/70 text-foreground\",\n        \"shadow-[0_0_0_0.5px_rgb(0_0_0/0.14),0_30px_70px_-20px_rgb(0_0_0/0.45)] backdrop-blur-3xl backdrop-saturate-150\",\n        \"dark:border-white/10 dark:bg-background/60 dark:shadow-[0_0_0_0.5px_rgb(0_0_0/0.7),0_30px_70px_-20px_rgb(0_0_0/0.8)]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype SpotlightSearchInputProps = React.ComponentProps<typeof CommandPrimitive.Input> & {\n  /** Content at the trailing end of the field, e.g. a Kbd hint or the top hit's kind. */\n  trailing?: React.ReactNode\n}\n\nfunction SpotlightSearchInput({ className, placeholder, trailing, ...props }: SpotlightSearchInputProps) {\n  const msg = useMessages()\n  placeholder ??= msg(\"spotlight-search.placeholder\", \"Spotlight Search\")\n  return (\n    <div data-slot=\"spotlight-search-field\" className=\"flex h-14 shrink-0 items-center gap-3 px-4\">\n      <SearchIcon className=\"size-5 shrink-0 text-foreground/50\" aria-hidden=\"true\" />\n      <CommandPrimitive.Input\n        data-slot=\"spotlight-search-input\"\n        placeholder={placeholder}\n        className={cn(\n          \"h-full min-w-0 flex-1 bg-transparent text-xl font-normal tracking-tight text-foreground outline-none placeholder:text-foreground/35 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        {...props}\n      />\n      {trailing ? <div className=\"flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">{trailing}</div> : null}\n    </div>\n  )\n}\n\ntype SpotlightSearchListProps = React.ComponentProps<typeof CommandPrimitive.List>\n\nfunction SpotlightSearchList({ className, ...props }: SpotlightSearchListProps) {\n  return (\n    <CommandPrimitive.List\n      data-slot=\"spotlight-search-list\"\n      className={cn(\n        \"max-h-[min(24rem,60dvh)] scroll-py-2 overflow-x-hidden overflow-y-auto overscroll-contain border-t border-foreground/[0.08] p-2 empty:hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SpotlightSearchEmpty({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n  return (\n    <CommandPrimitive.Empty\n      data-slot=\"spotlight-search-empty\"\n      className={cn(\"py-8 text-center text-sm text-foreground/50\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SpotlightSearchGroup({ ref, className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {\n  return (\n    <CommandPrimitive.Group\n      ref={(el: HTMLDivElement | null) => {\n        keepScrollInList(el?.querySelector<HTMLElement>(\"[cmdk-group-heading]\") ?? null)\n        setRef(ref, el)\n      }}\n      data-slot=\"spotlight-search-group\"\n      className={cn(\n        \"[&:not(:first-child)]:mt-1.5 [&_[cmdk-group-heading]]:px-2.5 [&_[cmdk-group-heading]]:pt-1 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:text-[11px] [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-foreground/45\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SpotlightSearchSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n  return <CommandPrimitive.Separator data-slot=\"spotlight-search-separator\" className={cn(\"mx-2.5 my-1.5 h-px bg-foreground/[0.08]\", className)} {...props} />\n}\n\ntype SpotlightSearchItemProps = React.ComponentProps<typeof CommandPrimitive.Item> & {\n  /** Icon, drawn in a small rounded tile. */\n  icon?: React.ReactNode\n  /** Classes for the icon tile (a gradient or color, like an app icon). */\n  iconClassName?: string\n  /** Secondary text at the trailing end: a kind (\"Application\"), a path, or a shortcut. */\n  detail?: React.ReactNode\n}\n\nfunction SpotlightSearchItem({ ref, icon, iconClassName, detail, value, className, children, ...props }: SpotlightSearchItemProps) {\n  return (\n    <CommandPrimitive.Item\n      ref={(el: HTMLDivElement | null) => {\n        keepScrollInList(el)\n        setRef(ref, el)\n      }}\n      data-slot=\"spotlight-search-item\"\n      // Match on the title only, not the detail text (\"Application\" would match almost any query).\n      value={value ?? (typeof children === \"string\" ? children : undefined)}\n      className={cn(\n        \"group/spotlight-item relative flex h-10 cursor-default items-center gap-3 rounded-[10px] px-2.5 text-sm outline-none select-none\",\n        \"data-[selected=true]:bg-[color-mix(in_oklch,var(--ring),black_28%)] data-[selected=true]:text-white data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {icon ? (\n        <span\n          data-slot=\"spotlight-search-item-icon\"\n          className={cn(\n            \"flex size-7 shrink-0 items-center justify-center rounded-[7px] bg-foreground/[0.07] text-foreground/75 shadow-[inset_0_0_0_0.5px_rgb(255_255_255/0.2)] [&_svg:not([class*='size-'])]:size-4\",\n            !iconClassName && \"group-data-[selected=true]/spotlight-item:bg-white/20 group-data-[selected=true]/spotlight-item:text-white\",\n            iconClassName\n          )}\n        >\n          {icon}\n        </span>\n      ) : null}\n      <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n      {detail ? (\n        <span data-slot=\"spotlight-search-item-detail\" className=\"shrink-0 text-xs text-muted-foreground group-data-[selected=true]/spotlight-item:text-white/90\">\n          {detail}\n        </span>\n      ) : null}\n    </CommandPrimitive.Item>\n  )\n}\n\ntype SpotlightSearchFooterProps = React.ComponentProps<\"div\">\n\n/** A hint row under the results, e.g. \"↵ Open · ⌘↵ Show in Folder\". */\nfunction SpotlightSearchFooter({ className, ...props }: SpotlightSearchFooterProps) {\n  return (\n    <div\n      data-slot=\"spotlight-search-footer\"\n      className={cn(\"flex h-9 shrink-0 items-center gap-3 border-t border-foreground/[0.08] px-4 text-xs text-foreground/50\", className)}\n      {...props}\n    />\n  )\n}\n\n/**\n * Calls `onTrigger` when ⌘ + key (Ctrl + key on Windows and Linux) is pressed anywhere on the page.\n * Pass `key = false` to turn it off.\n */\nfunction useSpotlightHotkey(onTrigger: () => void, key: string | false = \"k\") {\n  const callback = React.useRef(onTrigger)\n  React.useEffect(() => {\n    callback.current = onTrigger\n  })\n  React.useEffect(() => {\n    if (!key) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      if ((event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === key.toLowerCase()) {\n        event.preventDefault()\n        callback.current()\n      }\n    }\n    document.addEventListener(\"keydown\", onKeyDown)\n    return () => document.removeEventListener(\"keydown\", onKeyDown)\n  }, [key])\n}\n\ntype SpotlightSearchDialogProps = Omit<SpotlightSearchProps, \"title\"> & {\n  /** Controlled open state. */\n  open?: boolean\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean\n  /** Called when the dialog opens or closes. */\n  onOpenChange?: (open: boolean) => void\n  /** Key that toggles the dialog together with ⌘ (or Ctrl). false turns the shortcut off. */\n  hotkey?: string | false\n  /** Accessible dialog title (visually hidden). */\n  title?: string\n}\n\n/** SpotlightSearch in a Radix dialog near the top of the screen, toggled with ⌘K by default. Escape closes it. */\nfunction SpotlightSearchDialog({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  hotkey = \"k\",\n  title,\n  className,\n  children,\n  ...props\n}: SpotlightSearchDialogProps) {\n  const msg = useMessages()\n  title ??= msg(\"spotlight-search.title\", \"Spotlight Search\")\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen)\n  const open = openProp ?? uncontrolled\n  const setOpen = (next: boolean) => {\n    if (openProp === undefined) setUncontrolled(next)\n    onOpenChange?.(next)\n  }\n  useSpotlightHotkey(() => setOpen(!open), hotkey)\n\n  return (\n    <DialogPrimitive.Root open={open} onOpenChange={setOpen}>\n      <DialogPrimitive.Portal>\n        <DialogPrimitive.Overlay\n          data-slot=\"spotlight-search-overlay\"\n          className=\"fixed inset-0 z-50 bg-black/10 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 motion-reduce:animate-none dark:bg-black/30\"\n        />\n        <DialogPrimitive.Content\n          data-slot=\"spotlight-search-dialog\"\n          aria-describedby={undefined}\n          className=\"fixed top-[16dvh] left-1/2 z-50 w-[calc(100%-2rem)] max-w-[640px] -translate-x-1/2 outline-none data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-[0.97] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-[0.97] duration-150 motion-reduce:animate-none\"\n        >\n          <DialogPrimitive.Title className=\"sr-only\">{title}</DialogPrimitive.Title>\n          <SpotlightSearch className={className} {...props}>\n            {children}\n          </SpotlightSearch>\n        </DialogPrimitive.Content>\n      </DialogPrimitive.Portal>\n    </DialogPrimitive.Root>\n  )\n}\n\nexport {\n  SpotlightSearch,\n  SpotlightSearchDialog,\n  SpotlightSearchEmpty,\n  SpotlightSearchFooter,\n  SpotlightSearchGroup,\n  SpotlightSearchInput,\n  SpotlightSearchItem,\n  SpotlightSearchList,\n  SpotlightSearchSeparator,\n  useSpotlightHotkey,\n  type SpotlightSearchDialogProps,\n  type SpotlightSearchFooterProps,\n  type SpotlightSearchInputProps,\n  type SpotlightSearchItemProps,\n  type SpotlightSearchListProps,\n  type SpotlightSearchProps,\n}\n",
      "type": "registry:ui",
      "target": "@components/ballmac/spotlight-search.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "command palette",
      "search",
      "spotlight",
      "cmdk",
      "macos",
      "⌘k",
      "launcher",
      "keyboard"
    ],
    "version": "1.0.1",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Inline: <SpotlightSearch><SpotlightSearchInput /><SpotlightSearchList><SpotlightSearchEmpty/><SpotlightSearchGroup heading><SpotlightSearchItem icon iconClassName detail onSelect>Title</SpotlightSearchItem></SpotlightSearchGroup></SpotlightSearchList></SpotlightSearch>. Overlay: swap the root for <SpotlightSearchDialog open onOpenChange hotkey=\"k\">. Root props are cmdk Command props (filter, shouldFilter, value).",
      "whenToUse": [
        "A ⌘K command palette for navigation and actions in an app or docs site",
        "A Mac-style launcher in a product hero or desktop mockup",
        "Searching a mixed list of apps, documents and commands with keyboard-first navigation"
      ],
      "whenNotToUse": [
        "A plain search field for a results page (use input)",
        "Choosing one value in a form (use select or a combobox)"
      ],
      "composesWith": [
        "menu-bar",
        "mac-window",
        "dock"
      ],
      "a11y": [
        {
          "keys": "⌘K / Ctrl+K",
          "action": "Opens and closes SpotlightSearchDialog (hotkey prop; false disables)"
        },
        {
          "keys": "ArrowUp / ArrowDown",
          "action": "Moves the highlighted result, wrapping at the ends"
        },
        {
          "keys": "Enter",
          "action": "Runs the highlighted item's onSelect"
        },
        {
          "keys": "Escape",
          "action": "Closes the dialog and returns focus to where it was"
        }
      ],
      "customization": [
        "SpotlightSearchItem: icon + iconClassName (tile color), detail (kind, path or shortcut), keywords for extra matches",
        "SpotlightSearchInput: placeholder (default Spotlight Search), trailing slot",
        "SpotlightSearchFooter for key hints; SpotlightSearchSeparator between groups",
        "The highlight uses --ring, like the macOS accent color",
        "useSpotlightHotkey(callback, key) for your own trigger"
      ]
    },
    "examples": [
      "spotlight-search-demo",
      "spotlight-search-dialog"
    ],
    "url": "https://ui.ballmac.com/components/spotlight-search"
  },
  "categories": [
    "macos"
  ],
  "type": "registry:ui"
}