{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "project-scope-selector",
  "title": "Project Scope Selector",
  "description": "Construction project-scope selection in several layouts — card-grid wizard, hierarchical checklist tree, grouped multi-select combobox and a segmented category/items panel.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/button",
    "@nx-ui/checkbox",
    "@nx-ui/combobox",
    "@nx-ui/data-project-scopes",
    "@nx-ui/featured-icon",
    "@nx-ui/label",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/domain/project-scope-selector/index.ts",
      "content": "export {\n  ProjectScopeSelector,\n  type ProjectScopeSelectorWithLayoutProps,\n} from \"./project-scope-selector\"\nexport { ScopeCardGrid } from \"./scope-card-grid\"\nexport { ScopeChecklistTree } from \"./scope-checklist-tree\"\nexport { ScopeCombobox } from \"./scope-combobox\"\nexport { ScopeSegmentedPanel } from \"./scope-segmented-panel\"\nexport type { ProjectScopeSelectorProps, ScopeLayout } from \"./types\"\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/index.ts"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/project-scope-selector.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { ScopeCardGrid } from \"./scope-card-grid\"\nimport { ScopeChecklistTree } from \"./scope-checklist-tree\"\nimport { ScopeCombobox } from \"./scope-combobox\"\nimport { ScopeSegmentedPanel } from \"./scope-segmented-panel\"\nimport type { ProjectScopeSelectorProps, ScopeLayout } from \"./types\"\n\nexport interface ProjectScopeSelectorWithLayoutProps\n  extends ProjectScopeSelectorProps {\n  /** Which of the four layouts to render. Defaults to \"card-grid\". */\n  layout?: ScopeLayout\n}\n\nconst LAYOUTS: Record<\n  ScopeLayout,\n  React.ComponentType<ProjectScopeSelectorProps>\n> = {\n  \"card-grid\": ScopeCardGrid,\n  checklist: ScopeChecklistTree,\n  combobox: ScopeCombobox,\n  segmented: ScopeSegmentedPanel,\n}\n\n/**\n * ProjectScopeSelector — construction project-scope selection in four\n * interchangeable layouts:\n *\n *   - `card-grid`  — a featured-icon category grid that drills into activity cards\n *   - `checklist`  — an expandable tri-state checkbox tree\n *   - `combobox`   — a compact grouped multi-select with chips\n *   - `segmented`  — a category rail beside the active category's activities\n *\n * All are controlled the same way: `value` is the array of selected activity\n * keys, `onChange` receives the next array.\n */\nexport function ProjectScopeSelector({\n  layout = \"card-grid\",\n  className,\n  ...props\n}: ProjectScopeSelectorWithLayoutProps) {\n  const Layout = LAYOUTS[layout]\n  return <Layout className={cn(className)} {...props} />\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/project-scope-selector.tsx"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/scope-card-grid.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowLeft, Check, ChevronRight } from \"@untitledui/icons\"\n\nimport {\n  listScopeActivities,\n  listScopeCategories,\n  type ScopeActivity,\n  type ScopeCategory,\n} from \"@/lib/data/project-scopes\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { FeaturedIcon } from \"@/components/blocks/featured-icon\"\n\nimport { SCOPE_ICONS } from \"./scope-icon\"\nimport type { ProjectScopeSelectorProps } from \"./types\"\n\n/**\n * Card-grid wizard layout — categories as featured-icon cards; drilling into one\n * reveals its activities as toggleable cards. Each category card carries a badge\n * of how many of its activities are selected.\n */\nexport function ScopeCardGrid({\n  value,\n  onChange,\n  disabled = false,\n  className,\n}: ProjectScopeSelectorProps) {\n  const [activeCategory, setActiveCategory] = React.useState<string | null>(null)\n  const categories = listScopeCategories()\n  const selected = new Set(value)\n\n  function toggle(key: string) {\n    if (disabled) return\n    const next = new Set(value)\n    if (next.has(key)) next.delete(key)\n    else next.add(key)\n    onChange([...next])\n  }\n\n  function selectedInCategory(categoryKey: string): number {\n    return listScopeActivities(categoryKey).filter((a) => selected.has(a.key)).length\n  }\n\n  const category = activeCategory\n    ? categories.find((c) => c.key === activeCategory)\n    : null\n\n  if (category) {\n    const activities = listScopeActivities(category.key)\n    return (\n      <div className={cn(\"flex flex-col gap-4\", className)}>\n        <div className=\"flex items-center gap-2\">\n          <Button variant=\"ghost\" size=\"sm\" onClick={() => setActiveCategory(null)}>\n            <ArrowLeft className=\"size-4\" />\n            All categories\n          </Button>\n          <span className=\"text-sm font-semibold text-foreground\">{category.label}</span>\n        </div>\n        <div className=\"grid gap-3 sm:grid-cols-2\">\n          {activities.map((activity) => (\n            <ActivityCard\n              key={activity.key}\n              activity={activity}\n              selected={selected.has(activity.key)}\n              onToggle={toggle}\n            />\n          ))}\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\", className)}>\n      {categories.map((cat) => (\n        <CategoryCard\n          key={cat.key}\n          category={cat}\n          selectedCount={selectedInCategory(cat.key)}\n          onOpen={() => setActiveCategory(cat.key)}\n        />\n      ))}\n    </div>\n  )\n}\n\nfunction CategoryCard({\n  category,\n  selectedCount,\n  onOpen,\n}: {\n  category: ScopeCategory\n  selectedCount: number\n  onOpen: () => void\n}) {\n  const Icon = SCOPE_ICONS[category.icon]\n  return (\n    <button\n      type=\"button\"\n      onClick={onOpen}\n      className=\"flex flex-col gap-3 rounded-xl border border-border bg-background p-4 text-left shadow-xs transition-all hover:border-ring/50 hover:shadow-sm focus-visible:ring-4 focus-visible:ring-ring/25 focus-visible:outline-none\"\n    >\n      <div className=\"flex items-start justify-between gap-2\">\n        <FeaturedIcon icon={Icon} color=\"brand\" size=\"md\" />\n        {selectedCount > 0 && <Badge variant=\"brand\">{selectedCount}</Badge>}\n      </div>\n      <div className=\"flex flex-col gap-0.5\">\n        <span className=\"flex items-center justify-between text-sm font-semibold text-foreground\">\n          {category.label}\n          <ChevronRight className=\"size-4 shrink-0 text-muted-foreground\" />\n        </span>\n        <span className=\"text-xs text-muted-foreground\">{category.description}</span>\n      </div>\n    </button>\n  )\n}\n\nfunction ActivityCard({\n  activity,\n  selected,\n  onToggle,\n}: {\n  activity: ScopeActivity\n  selected: boolean\n  onToggle: (key: string) => void\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={selected}\n      onClick={() => onToggle(activity.key)}\n      className={cn(\n        \"flex flex-col gap-1 rounded-xl border p-4 text-left shadow-xs transition-all focus-visible:ring-4 focus-visible:ring-ring/25 focus-visible:outline-none\",\n        selected\n          ? \"border-primary bg-primary/5 ring-1 ring-primary\"\n          : \"border-border bg-background hover:border-ring/50 hover:shadow-sm\"\n      )}\n    >\n      <span className=\"flex items-center justify-between gap-2\">\n        <span className=\"text-sm font-semibold text-foreground\">{activity.label}</span>\n        {selected && <Check className=\"size-4 shrink-0 text-primary\" />}\n      </span>\n      <span className=\"text-xs text-muted-foreground\">{activity.description}</span>\n    </button>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/scope-card-grid.tsx"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/scope-checklist-tree.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, ChevronRight } from \"@untitledui/icons\"\n\nimport {\n  listScopeActivities,\n  listScopeCategories,\n} from \"@/lib/data/project-scopes\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\n\nimport type { ProjectScopeSelectorProps } from \"./types\"\n\n/**\n * Checklist-tree layout — categories as expandable rows with a tri-state parent\n * checkbox (checked / indeterminate / unchecked) that mirrors and drives its\n * activities. Toggling a category selects or clears all its activities at once.\n */\nexport function ScopeChecklistTree({\n  value,\n  onChange,\n  disabled = false,\n  className,\n}: ProjectScopeSelectorProps) {\n  const categories = listScopeCategories()\n  const selected = new Set(value)\n  const [expanded, setExpanded] = React.useState<Set<string>>(\n    () => new Set(categories.map((c) => c.key))\n  )\n\n  function setMany(keys: string[], on: boolean) {\n    const next = new Set(value)\n    for (const k of keys) {\n      if (on) next.add(k)\n      else next.delete(k)\n    }\n    onChange([...next])\n  }\n\n  function toggleActivity(key: string, on: boolean) {\n    setMany([key], on)\n  }\n\n  function toggleExpanded(key: string) {\n    setExpanded((prev) => {\n      const next = new Set(prev)\n      if (next.has(key)) next.delete(key)\n      else next.add(key)\n      return next\n    })\n  }\n\n  return (\n    <div className={cn(\"flex flex-col divide-y divide-border rounded-xl border border-border\", className)}>\n      {categories.map((category) => {\n        const activities = listScopeActivities(category.key)\n        const selectedCount = activities.filter((a) => selected.has(a.key)).length\n        const all = selectedCount === activities.length\n        const some = selectedCount > 0 && !all\n        const isOpen = expanded.has(category.key)\n\n        return (\n          <div key={category.key}>\n            <div className=\"flex items-center gap-2.5 px-3 py-2.5\">\n              <Checkbox\n                checked={all}\n                indeterminate={some}\n                disabled={disabled}\n                onCheckedChange={(checked) =>\n                  setMany(\n                    activities.map((a) => a.key),\n                    checked === true\n                  )\n                }\n                aria-label={`Select all ${category.label}`}\n              />\n              <button\n                type=\"button\"\n                onClick={() => toggleExpanded(category.key)}\n                className=\"flex flex-1 items-center gap-2 text-left\"\n                aria-expanded={isOpen}\n              >\n                {isOpen ? (\n                  <ChevronDown className=\"size-4 shrink-0 text-muted-foreground\" />\n                ) : (\n                  <ChevronRight className=\"size-4 shrink-0 text-muted-foreground\" />\n                )}\n                <span className=\"text-sm font-semibold text-foreground\">{category.label}</span>\n                {selectedCount > 0 && (\n                  <Badge variant=\"brand\" className=\"ml-1\">\n                    {selectedCount}/{activities.length}\n                  </Badge>\n                )}\n              </button>\n            </div>\n            {isOpen && (\n              <ul className=\"flex flex-col gap-1 pb-2 pl-10\">\n                {activities.map((activity) => (\n                  <li key={activity.key}>\n                    <label\n                      className={cn(\n                        \"flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-muted\",\n                        disabled && \"cursor-not-allowed opacity-60\"\n                      )}\n                    >\n                      <Checkbox\n                        checked={selected.has(activity.key)}\n                        disabled={disabled}\n                        onCheckedChange={(checked) =>\n                          toggleActivity(activity.key, checked === true)\n                        }\n                      />\n                      <span className=\"text-sm text-foreground\">{activity.label}</span>\n                    </label>\n                  </li>\n                ))}\n              </ul>\n            )}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/scope-checklist-tree.tsx"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/scope-combobox.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport {\n  findScopeActivity,\n  listScopeActivities,\n  listScopeCategories,\n  type ScopeActivity,\n} from \"@/lib/data/project-scopes\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  Combobox,\n  ComboboxChip,\n  ComboboxChips,\n  ComboboxChipsInput,\n  ComboboxCollection,\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxGroup,\n  ComboboxItem,\n  ComboboxLabel,\n  ComboboxList,\n  ComboboxValue,\n  useComboboxAnchor,\n} from \"@/components/ui/combobox\"\nimport { Label } from \"@/components/ui/label\"\n\nimport type { ProjectScopeSelectorProps } from \"./types\"\n\ninterface ScopeGroup {\n  value: string\n  items: ScopeActivity[]\n}\n\n/** Activity items grouped by their category, for the grouped combobox list. */\nconst GROUPS: ScopeGroup[] = listScopeCategories().map((category) => ({\n  value: category.label,\n  items: listScopeActivities(category.key),\n}))\n\n/**\n * Combobox layout — a compact grouped multi-select. Type to filter across every\n * activity (grouped by category in the list); each choice becomes a removable\n * chip. Best where scope is a secondary field rather than the main focus.\n */\nexport function ScopeCombobox({\n  value,\n  onChange,\n  disabled = false,\n  className,\n}: ProjectScopeSelectorProps) {\n  const anchor = useComboboxAnchor()\n  const labelId = React.useId()\n\n  return (\n    <div className={cn(\"flex w-full max-w-lg flex-col gap-1.5\", className)}>\n      <Label id={labelId} className=\"block\">\n        Project scope\n      </Label>\n      <Combobox\n        items={GROUPS}\n        multiple\n        value={value}\n        disabled={disabled}\n        onValueChange={(next: string[]) => onChange(next)}\n        // Items in the list are ScopeActivity objects (for filtering); the\n        // selected value/chips are string keys. Handle both shapes so filter\n        // text and chip labels are always the activity label.\n        itemToStringLabel={(item: ScopeActivity | string) =>\n          typeof item === \"string\"\n            ? (findScopeActivity(item)?.label ?? item)\n            : item.label\n        }\n      >\n        <ComboboxChips ref={anchor} aria-labelledby={labelId}>\n          <ComboboxValue>\n            {(selected: string[]) => (\n              <>\n                {selected.map((key) => (\n                  <ComboboxChip key={key} aria-label={findScopeActivity(key)?.label}>\n                    {findScopeActivity(key)?.label ?? key}\n                  </ComboboxChip>\n                ))}\n                <ComboboxChipsInput placeholder=\"Add scope…\" />\n              </>\n            )}\n          </ComboboxValue>\n        </ComboboxChips>\n        <ComboboxContent anchor={anchor}>\n          <ComboboxEmpty>No activities found.</ComboboxEmpty>\n          <ComboboxList>\n            {(group: ScopeGroup) => (\n              <ComboboxGroup key={group.value} items={group.items}>\n                <ComboboxLabel>{group.value}</ComboboxLabel>\n                <ComboboxCollection>\n                  {(activity: ScopeActivity) => (\n                    <ComboboxItem key={activity.key} value={activity.key}>\n                      {activity.label}\n                    </ComboboxItem>\n                  )}\n                </ComboboxCollection>\n              </ComboboxGroup>\n            )}\n          </ComboboxList>\n        </ComboboxContent>\n      </Combobox>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/scope-combobox.tsx"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/scope-icon.tsx",
      "content": "import {\n  Building07,\n  Droplets01,\n  Feather,\n  LayersTwo01,\n  RefreshCcw01,\n  Tool01,\n  Triangle,\n  Zap,\n} from \"@untitledui/icons\"\n\nimport type { ScopeIconKey } from \"@/lib/data/project-scopes\"\n\n/** Maps a data-layer `ScopeIconKey` to its `@untitledui/icons` component. */\nexport const SCOPE_ICONS: Record<\n  ScopeIconKey,\n  React.ComponentType<{ className?: string }>\n> = {\n  Tool01,\n  Zap,\n  Triangle,\n  RefreshCcw01,\n  Droplets01,\n  LayersTwo01,\n  Building07,\n  Feather,\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/scope-icon.tsx"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/scope-segmented-panel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport {\n  listScopeActivities,\n  listScopeCategories,\n} from \"@/lib/data/project-scopes\"\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\n\nimport { SCOPE_ICONS } from \"./scope-icon\"\nimport type { ProjectScopeSelectorProps } from \"./types\"\n\n/**\n * Segmented-panel layout — categories listed on the left, the active category's\n * activities on the right. Each category row shows a badge of its selected\n * count; the split keeps a large scope legible without deep drilling.\n */\nexport function ScopeSegmentedPanel({\n  value,\n  onChange,\n  disabled = false,\n  className,\n}: ProjectScopeSelectorProps) {\n  const categories = listScopeCategories()\n  const [activeKey, setActiveKey] = React.useState(categories[0]?.key ?? \"\")\n  const selected = new Set(value)\n\n  function toggle(key: string, on: boolean) {\n    const next = new Set(value)\n    if (on) next.add(key)\n    else next.delete(key)\n    onChange([...next])\n  }\n\n  function selectedInCategory(categoryKey: string): number {\n    return listScopeActivities(categoryKey).filter((a) => selected.has(a.key)).length\n  }\n\n  const activeActivities = listScopeActivities(activeKey)\n\n  return (\n    <div\n      className={cn(\n        \"grid overflow-hidden rounded-xl border border-border sm:grid-cols-[minmax(180px,1fr)_2fr]\",\n        className\n      )}\n    >\n      {/* Category rail */}\n      <ul className=\"flex flex-col border-b border-border bg-muted/30 sm:border-r sm:border-b-0\">\n        {categories.map((category) => {\n          const Icon = SCOPE_ICONS[category.icon]\n          const count = selectedInCategory(category.key)\n          const isActive = category.key === activeKey\n          return (\n            <li key={category.key}>\n              <button\n                type=\"button\"\n                onClick={() => setActiveKey(category.key)}\n                aria-current={isActive}\n                className={cn(\n                  \"flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-sm transition-colors\",\n                  isActive\n                    ? \"bg-background font-semibold text-foreground\"\n                    : \"text-muted-foreground hover:bg-background/60\"\n                )}\n              >\n                <Icon className=\"size-4 shrink-0\" />\n                <span className=\"flex-1 truncate\">{category.label}</span>\n                {count > 0 && <Badge variant=\"brand\">{count}</Badge>}\n              </button>\n            </li>\n          )\n        })}\n      </ul>\n\n      {/* Activity list for the active category */}\n      <div className=\"flex flex-col gap-1 p-3\">\n        {activeActivities.map((activity) => (\n          <label\n            key={activity.key}\n            className={cn(\n              \"flex cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 hover:bg-muted\",\n              disabled && \"cursor-not-allowed opacity-60\"\n            )}\n          >\n            <Checkbox\n              className=\"mt-0.5\"\n              checked={selected.has(activity.key)}\n              disabled={disabled}\n              onCheckedChange={(checked) => toggle(activity.key, checked === true)}\n            />\n            <span className=\"flex flex-col\">\n              <span className=\"text-sm font-medium text-foreground\">{activity.label}</span>\n              <span className=\"text-xs text-muted-foreground\">{activity.description}</span>\n            </span>\n          </label>\n        ))}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/scope-segmented-panel.tsx"
    },
    {
      "path": "components/blocks/domain/project-scope-selector/types.ts",
      "content": "/**\n * Project scope selector — shared types.\n *\n * Every layout is controlled the same way: a `value` array of selected activity\n * keys and an `onChange` that receives the next array. Selection is over leaf\n * `ScopeActivity` keys (e.g. \"earthworks.cut-fill\"); categories are grouping\n * only.\n */\n\nexport interface ProjectScopeSelectorProps {\n  /** Controlled selection — the set of selected activity keys. */\n  value: string[]\n  onChange: (next: string[]) => void\n  disabled?: boolean\n  className?: string\n}\n\n/** The four selectable layouts. */\nexport type ScopeLayout = \"card-grid\" | \"checklist\" | \"combobox\" | \"segmented\"\n",
      "type": "registry:component",
      "target": "components/blocks/domain/project-scope-selector/types.ts"
    }
  ],
  "type": "registry:block"
}