{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "desktop-7",
  "title": "Desktop 7: welcome window",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The window a desktop app opens with when nothing is open: name and version, ways to start (new, open, clone) with shortcuts, and recent projects to search, open with Enter and remove from the list.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/app-window.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/checkbox.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/feel-presets.json",
    "https://ui.ballmac.com/r/shortcut.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/desktop-7/desktop-7.tsx",
      "content": "// Ballmac UI: Desktop 7. https://ui.ballmac.com/blocks/desktop-7\n\"use client\"\n\nimport * as React from \"react\"\nimport { FolderClock, FolderOpen, GitBranch, Plus, Search, Sparkles, X } from \"lucide-react\"\n\nimport { AppWindow, AppWindowContent, AppWindowTitleBar } from \"@/components/ballmac/app-window\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { Checkbox } from \"@/components/ballmac/checkbox\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { useFeel, type FeelId } from \"@/lib/ballmac/feel\"\nimport { formatShortcut, usesMacKeys } from \"@/lib/ballmac/shortcut\"\nimport { cn } from \"@/lib/utils\"\n\ntype Desktop7Action = { id: string; label: string; description?: string; icon?: React.ReactNode; shortcut?: string }\n\ntype Desktop7Recent = {\n  id: string\n  name: string\n  /** Where it lives, shown under the name. */\n  path: string\n  /** When it was last opened, already written for people (\"2 hours ago\"). */\n  opened?: string\n}\n\ntype Desktop7Props = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  StateProps & {\n    /** Which platform the window handles like. Defaults to the page's feel. */\n    feel?: FeelId\n    /** Draw the window frame. Turn off inside a real Tauri or Electron window that already has one. */\n    frame?: boolean\n    appName?: string\n    version?: string\n    /** The app icon. A generated one is drawn when you leave it out. */\n    icon?: React.ReactNode\n    /** The ways to start: new, open, clone. */\n    actions?: Desktop7Action[]\n    /** Recently opened projects or documents, most recent first. */\n    recents?: Desktop7Recent[]\n    /** Called by an action. */\n    onAction?: (id: string) => void\n    /** Called when a recent item is opened with a click, double-click or Enter. */\n    onOpenRecent?: (id: string) => void\n    /** Called when a recent item is removed from the list. Without it, removing is not offered. */\n    onRemoveRecent?: (id: string) => void\n    /** Whether \"Show this window when the app opens\" is ticked (controlled). */\n    showOnLaunch?: boolean\n    onShowOnLaunchChange?: (show: boolean) => void\n    onClose?: () => void\n  }\n\nconst defaultActions: Desktop7Action[] = [\n  { id: \"new\", label: \"New project\", description: \"Start from an empty folder\", icon: <Plus />, shortcut: \"Mod+N\" },\n  { id: \"open\", label: \"Open…\", description: \"A folder already on this computer\", icon: <FolderOpen />, shortcut: \"Mod+O\" },\n  { id: \"clone\", label: \"Clone repository\", description: \"Copy a project from a Git URL\", icon: <GitBranch />, shortcut: \"Mod+Shift+C\" },\n]\n\nconst sampleRecents: Desktop7Recent[] = [\n  { id: \"1\", name: \"acme-web\", path: \"~/Code/acme-web\", opened: \"12 minutes ago\" },\n  { id: \"2\", name: \"design-tokens\", path: \"~/Code/design-tokens\", opened: \"Yesterday\" },\n  { id: \"3\", name: \"release-notes\", path: \"~/Documents/release-notes\", opened: \"3 days ago\" },\n  { id: \"4\", name: \"billing-service\", path: \"~/Code/billing-service\", opened: \"Last week\" },\n  { id: \"5\", name: \"onboarding-research\", path: \"~/Documents/onboarding-research\", opened: \"2 weeks ago\" },\n]\n\nfunction RecentSkeleton() {\n  return (\n    <div className=\"grid gap-1 p-2\" aria-hidden=\"true\">\n      {[0, 1, 2, 3].map((i) => (\n        <div key={i} className=\"flex items-center gap-3 px-2 py-2\">\n          <Skeleton className=\"size-8 rounded-md\" />\n          <div className=\"grid flex-1 gap-1.5\">\n            <Skeleton className=\"h-3.5 w-2/5\" />\n            <Skeleton className=\"h-3 w-3/5\" />\n          </div>\n        </div>\n      ))}\n    </div>\n  )\n}\n\n/**\n * The window a desktop app opens with when nothing is open: the app's name and version, ways to start (new, open, clone) with their\n * shortcuts, and recent projects to search, open with Enter and remove with Delete.\n */\nfunction Desktop7({\n  feel: feelProp,\n  frame = true,\n  appName = \"Acme Studio\",\n  version = \"3.2.0\",\n  icon,\n  actions = defaultActions,\n  recents: recentsProp = sampleRecents,\n  onAction,\n  onOpenRecent,\n  onRemoveRecent,\n  showOnLaunch: showProp,\n  onShowOnLaunchChange,\n  onClose,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Desktop7Props) {\n  const page = useFeel().feel\n  const feel = feelProp ?? page\n  const mac = usesMacKeys(feel)\n  const win = feel === \"windows\"\n  const id = React.useId()\n  const [recents, setRecents] = React.useState(recentsProp)\n  React.useEffect(() => setRecents(recentsProp), [recentsProp])\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n  const [showLocal, setShowLocal] = React.useState(true)\n  const show = showProp ?? showLocal\n  const q = query.trim().toLowerCase()\n  const visible = recents.filter((r) => !q || `${r.name} ${r.path}`.toLowerCase().includes(q))\n  const current = visible[Math.min(active, visible.length - 1)]\n  React.useEffect(() => setActive(0), [q])\n\n  const remove = (r: Desktop7Recent) => {\n    setRecents((list) => list.filter((x) => x.id !== r.id))\n    onRemoveRecent?.(r.id)\n  }\n\n  const listKeys = (e: React.KeyboardEvent) => {\n    if (!visible.length) return\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      setActive((a) => Math.min(a + 1, visible.length - 1))\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault()\n      setActive((a) => Math.max(a - 1, 0))\n    } else if (e.key === \"Enter\" && current) {\n      e.preventDefault()\n      onOpenRecent?.(current.id)\n    } else if ((e.key === \"Delete\" || e.key === \"Backspace\") && current && onRemoveRecent && e.currentTarget.tagName !== \"INPUT\") {\n      e.preventDefault()\n      remove(current)\n    }\n  }\n\n  const content = (\n    <div className={cn(\"grid min-h-0 flex-1\", \"sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]\")}>\n      <div className={cn(\"flex flex-col gap-6 p-6\", mac && \"items-center text-center sm:justify-center\")}>\n        <div className={cn(\"flex gap-4\", mac ? \"flex-col items-center\" : \"items-center\")}>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"from-primary to-primary/70 text-primary-foreground flex shrink-0 items-center justify-center rounded-[22%] bg-linear-to-b shadow-[0_2px_6px_rgb(0_0_0/0.18),inset_0_1px_0_rgb(255_255_255/0.25)] [&_svg]:size-1/2\",\n              mac ? \"size-20\" : \"size-12\"\n            )}\n          >\n            {icon ?? <Sparkles />}\n          </span>\n          <div>\n            <h2 id={`${id}-title`} className=\"text-xl font-semibold tracking-tight\">\n              {win ? `Welcome to ${appName}` : appName}\n            </h2>\n            <p className=\"text-muted-foreground text-xs tabular-nums\">Version {version}</p>\n          </div>\n        </div>\n        <ul aria-label=\"Start\" className={cn(\"grid w-full gap-1\", mac && \"max-w-64\")}>\n          {actions.map((a) => (\n            <li key={a.id}>\n              <button\n                type=\"button\"\n                onClick={() => onAction?.(a.id)}\n                className=\"hover:bg-accent focus-visible:ring-ring/50 flex w-full items-center gap-3 rounded-lg px-3 py-2 text-start outline-none focus-visible:ring-[3px]\"\n              >\n                <span aria-hidden=\"true\" className=\"bg-muted flex size-8 shrink-0 items-center justify-center rounded-md border [&_svg]:size-4\">\n                  {a.icon}\n                </span>\n                <span className=\"min-w-0 flex-1\">\n                  <span className=\"block text-sm font-medium\">{a.label}</span>\n                  {a.description ? <span className=\"text-muted-foreground block truncate text-xs\">{a.description}</span> : null}\n                </span>\n                {a.shortcut ? (\n                  <kbd dir=\"ltr\" className=\"text-muted-foreground hidden font-sans text-xs sm:block\">\n                    {formatShortcut(a.shortcut, feel)}\n                  </kbd>\n                ) : null}\n              </button>\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      <div className=\"bg-muted/40 flex min-h-0 flex-col border-t sm:border-s sm:border-t-0\">\n        <div className=\"flex items-center gap-2 p-3\">\n          <h3 className=\"flex-1 text-xs font-semibold\">Recent</h3>\n        </div>\n        <div className=\"px-3 pb-2\">\n          <div className=\"bg-background focus-within:ring-ring/50 flex h-8 items-center gap-2 rounded-md border px-2 focus-within:ring-[3px]\">\n            <Search aria-hidden=\"true\" className=\"text-muted-foreground size-3.5\" />\n            <input\n              role=\"combobox\"\n              aria-label=\"Search recent projects\"\n              aria-expanded={state === \"ready\" && visible.length > 0}\n              aria-controls={`${id}-recents`}\n              aria-activedescendant={state === \"ready\" && current ? `${id}-r-${current.id}` : undefined}\n              aria-autocomplete=\"list\"\n              value={query}\n              disabled={state !== \"ready\"}\n              onChange={(e) => setQuery(e.target.value)}\n              onKeyDown={listKeys}\n              placeholder=\"Search\"\n              className=\"placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-[13px] outline-none\"\n            />\n          </div>\n        </div>\n        <StateView\n          state={state === \"ready\" && !recents.length ? \"empty\" : state}\n          onRetry={onRetry}\n          bare\n          label=\"Loading recent projects\"\n          loading={<RecentSkeleton />}\n          empty={{ title: emptyState?.title ?? \"Nothing opened yet\", description: emptyState?.description ?? \"Projects you open will be listed here.\", icon: emptyState?.icon ?? <FolderClock />, action: emptyState?.action }}\n          error={{ title: errorState?.title ?? \"Recent projects could not be read\", ...errorState }}\n          className=\"min-h-0 flex-1 overflow-hidden\"\n        >\n          {visible.length ? (\n            <ul\n              id={`${id}-recents`}\n              role=\"listbox\"\n              aria-label=\"Recent projects\"\n              tabIndex={0}\n              aria-activedescendant={current ? `${id}-r-${current.id}` : undefined}\n              onKeyDown={listKeys}\n              className=\"grid h-full content-start gap-0.5 overflow-y-auto rounded-md px-2 pb-2 outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50\"\n            >\n              {visible.map((r, i) => {\n                const sel = current?.id === r.id\n                return (\n                  <li\n                    key={r.id}\n                    id={`${id}-r-${r.id}`}\n                    role=\"option\"\n                    aria-selected={sel}\n                    onMouseMove={() => setActive(i)}\n                    onClick={() => onOpenRecent?.(r.id)}\n                    className={cn(\"flex cursor-default items-center gap-3 rounded-md px-2 py-1.5\", sel && (mac ? \"bg-primary text-primary-foreground\" : \"bg-foreground/[0.07]\"))}\n                  >\n                    <span aria-hidden=\"true\" className={cn(\"flex size-8 shrink-0 items-center justify-center rounded-md border text-xs font-semibold uppercase\", sel && mac ? \"border-primary-foreground/30\" : \"bg-background\")}>\n                      {r.name.slice(0, 2)}\n                    </span>\n                    <span className=\"min-w-0 flex-1\">\n                      <span className=\"block truncate text-[13px] font-medium\">{r.name}</span>\n                      <span dir=\"ltr\" className={cn(\"block truncate text-start text-[11px]\", sel && mac ? \"text-primary-foreground/75\" : \"text-muted-foreground\")}>\n                        {r.path}\n                      </span>\n                    </span>\n                    {r.opened ? <span className={cn(\"hidden shrink-0 text-[11px] md:block\", sel && mac ? \"text-primary-foreground/75\" : \"text-muted-foreground\")}>{r.opened}</span> : null}\n                  </li>\n                )\n              })}\n            </ul>\n          ) : (\n            <p className=\"text-muted-foreground px-4 py-8 text-center text-sm\">No recent projects match “{query}”.</p>\n          )}\n        </StateView>\n        {onRemoveRecent && current && state === \"ready\" ? (\n          <div className=\"flex items-center justify-between gap-2 border-t px-3 py-2\">\n            <p className=\"text-muted-foreground truncate text-xs\">\n              <span className=\"sr-only\">Selected:</span>{\" \"}\n              {current.name}\n            </p>\n            <Button variant=\"ghost\" size=\"sm\" onClick={() => remove(current)}>\n              <X aria-hidden=\"true\" />\n              Remove from list\n            </Button>\n          </div>\n        ) : null}\n      </div>\n    </div>\n  )\n\n  const footer = (\n    <div className=\"flex items-center gap-2 border-t px-4 py-2.5\">\n      <Checkbox\n        id={`${id}-show`}\n        checked={show}\n        onCheckedChange={(c) => {\n          setShowLocal(c === true)\n          onShowOnLaunchChange?.(c === true)\n        }}\n      />\n      <label htmlFor={`${id}-show`} className=\"text-muted-foreground text-xs\">\n        Show this window when {appName} opens\n      </label>\n    </div>\n  )\n\n  if (!frame)\n    return (\n      <div data-slot=\"desktop-7\" data-state={state} data-feel={feelProp} role=\"region\" aria-labelledby={`${id}-title`} className={cn(\"bg-card flex flex-col overflow-hidden rounded-xl border\", className)} {...frameProps(props)}>\n        {content}\n        {footer}\n      </div>\n    )\n  return (\n    <AppWindow data-slot=\"desktop-7\" data-state={state} feel={feel} onClose={onClose} role=\"region\" aria-labelledby={`${id}-title`} className={cn(\"h-[30rem] w-full max-w-3xl\", className)} {...frameProps(props)}>\n      <AppWindowTitleBar title={win ? \"Welcome\" : \"\"} icon={<Sparkles />} className={cn(!win && \"border-b-0\")} />\n      <AppWindowContent className=\"flex flex-col overflow-hidden\">\n        {content}\n        {footer}\n      </AppWindowContent>\n    </AppWindow>\n  )\n}\n\nexport { Desktop7, type Desktop7Props, type Desktop7Action, type Desktop7Recent }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/desktop-7/desktop-7.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "desktop",
      "welcome",
      "start",
      "recent",
      "projects",
      "launcher",
      "tauri",
      "electron",
      "macos",
      "windows",
      "feel"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "A welcome or launcher window. <Desktop7 appName version actions={[{ id, label, description, icon, shortcut: 'Mod+N' }]} recents={[{ id, name, path, opened }]} onAction onOpenRecent onRemoveRecent showOnLaunch onShowOnLaunchChange />. Recents are a combobox with a listbox: type to search, arrows to move, Enter to open.",
      "whenToUse": [
        "The start window of an editor, IDE or document app built with Tauri or Electron",
        "Choosing a recent project before the main window opens"
      ],
      "whenNotToUse": [
        "A dashboard home inside a web app (use dashboard-1)",
        "Signing in (use login-1)"
      ],
      "composesWith": [
        "desktop-1",
        "desktop-6",
        "app-window",
        "feel-auto"
      ],
      "a11y": [
        {
          "keys": "Up / Down in the search field",
          "action": "Moves through recent projects; the selection is announced"
        },
        {
          "keys": "Enter",
          "action": "Opens the selected project"
        },
        {
          "keys": "Tab",
          "action": "Reaches each start action and Remove from list for the selected project"
        }
      ],
      "customization": [
        "feel: 'macos' | 'windows' (defaults to the page's feel); shortcuts are written in the platform's notation",
        "Tauri: keep recents in the store plugin; open with the dialog plugin's open({ directory: true })"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading recents",
        "example": "desktop-7-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton rows in the Recent column while the list is read; the start actions work straight away."
      },
      {
        "id": "empty",
        "title": "First launch",
        "example": "desktop-7-empty",
        "trigger": "state=\"empty\" (or recents={[]})",
        "note": "Says opened projects will be listed; emptyState can add an action."
      },
      {
        "id": "error",
        "title": "Could not read recents",
        "example": "desktop-7-error",
        "trigger": "state=\"error\"",
        "note": "An alert in the Recent column with Try again (onRetry); the start actions still work."
      }
    ],
    "examples": [
      "desktop-7-demo",
      "desktop-7-windows",
      "desktop-7-loading",
      "desktop-7-empty",
      "desktop-7-error"
    ],
    "url": "https://ui.ballmac.com/components/desktop-7"
  },
  "categories": [
    "blocks",
    "desktop"
  ],
  "type": "registry:block"
}