{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "permission-selector",
  "title": "Permission Selector",
  "description": "A composable segmented permission control with an animated sliding pill — read/write levels and deny/neutral/allow toggles.",
  "dependencies": ["motion"],
  "files": [
    {
      "path": "components/ui/permission-selector.tsx",
      "content": "\"use client\";\n\nimport { domMax, LazyMotion, m } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype PermissionTone = \"neutral\" | \"success\" | \"danger\" | \"warning\";\n\nconst SPRING = { type: \"spring\", bounce: 0.2, duration: 0.4 } as const;\n\nconst TONE_TEXT: Record<PermissionTone, string> = {\n  neutral: \"text-foreground\",\n  success: \"text-emerald-600 dark:text-emerald-400\",\n  danger: \"text-destructive\",\n  warning: \"text-amber-600 dark:text-amber-500\",\n};\n\nconst TONE_PILL: Record<PermissionTone, string> = {\n  neutral: \"bg-background ring-border\",\n  success: \"bg-emerald-500/10 ring-emerald-500/30\",\n  danger: \"bg-destructive/10 ring-destructive/30\",\n  warning: \"bg-amber-500/10 ring-amber-500/30\",\n};\n\ninterface PermissionRowContextValue {\n  value: string | undefined;\n  select: (value: string) => void;\n  layoutId: string;\n  disabled: boolean;\n}\n\nconst PermissionRowContext = createContext<PermissionRowContextValue | null>(\n  null\n);\n\nfunction usePermissionRow() {\n  const context = useContext(PermissionRowContext);\n  if (!context) {\n    throw new Error(\"PermissionOption must be used within a PermissionRow\");\n  }\n  return context;\n}\n\ninterface PermissionSelectorProps {\n  children: ReactNode;\n  label?: string;\n  className?: string;\n}\n\nfunction PermissionSelector({\n  children,\n  label = \"Permissions\",\n  className,\n}: PermissionSelectorProps) {\n  return (\n    <fieldset\n      className={cn(\n        \"w-full divide-y overflow-hidden rounded-xl border bg-background\",\n        className\n      )}\n    >\n      <legend className=\"sr-only\">{label}</legend>\n      {children}\n    </fieldset>\n  );\n}\n\ninterface PermissionRowProps {\n  children: ReactNode;\n  label: ReactNode;\n  description?: ReactNode;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  disabled?: boolean;\n  className?: string;\n}\n\nfunction PermissionRow({\n  children,\n  label,\n  description,\n  value,\n  defaultValue,\n  onValueChange,\n  disabled = false,\n  className,\n}: PermissionRowProps) {\n  const layoutId = useId();\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const activeValue = value ?? internalValue;\n\n  const select = (next: string) => {\n    setInternalValue(next);\n    onValueChange?.(next);\n  };\n\n  const context: PermissionRowContextValue = {\n    value: activeValue,\n    select,\n    layoutId,\n    disabled,\n  };\n\n  return (\n    <PermissionRowContext.Provider value={context}>\n      <LazyMotion features={domMax}>\n        <div\n          className={cn(\n            \"flex items-center justify-between gap-4 px-4 py-3\",\n            className\n          )}\n        >\n          <div className=\"flex min-w-0 flex-col gap-0.5\">\n            <span className=\"font-medium text-foreground text-sm\">{label}</span>\n            {description && (\n              <span className=\"text-muted-foreground text-xs\">\n                {description}\n              </span>\n            )}\n          </div>\n          <div\n            aria-label={typeof label === \"string\" ? label : undefined}\n            className=\"flex shrink-0 items-center gap-0.5 rounded-lg border bg-muted/40 p-0.5\"\n            role=\"radiogroup\"\n          >\n            {children}\n          </div>\n        </div>\n      </LazyMotion>\n    </PermissionRowContext.Provider>\n  );\n}\n\ninterface PermissionOptionProps {\n  value: string;\n  children: ReactNode;\n  tone?: PermissionTone;\n  disabled?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n}\n\nfunction PermissionOption({\n  value,\n  children,\n  tone = \"neutral\",\n  disabled: optionDisabled,\n  className,\n  \"aria-label\": ariaLabel,\n}: PermissionOptionProps) {\n  const {\n    value: selected,\n    select,\n    layoutId,\n    disabled: rowDisabled,\n  } = usePermissionRow();\n  const active = selected === value;\n  const disabled = optionDisabled ?? rowDisabled;\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a single-select segmented control is the radiogroup/radio ARIA pattern; native radios can't hold custom pill content.\n    <button\n      aria-checked={active}\n      aria-label={ariaLabel}\n      className={cn(\n        \"relative flex min-w-7 cursor-pointer items-center justify-center rounded-md px-2.5 py-1 font-medium text-sm outline-none transition-colors duration-200 ease-out focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-4\",\n        active\n          ? TONE_TEXT[tone]\n          : \"text-muted-foreground hover:text-foreground\",\n        className\n      )}\n      disabled={disabled}\n      onClick={() => select(value)}\n      role=\"radio\"\n      type=\"button\"\n    >\n      {active && (\n        <m.span\n          className={cn(\n            \"absolute inset-0 rounded-md shadow-sm ring-1\",\n            TONE_PILL[tone]\n          )}\n          layoutId={layoutId}\n          transition={SPRING}\n        />\n      )}\n      <span className=\"relative z-10 flex items-center gap-1.5\">\n        {children}\n      </span>\n    </button>\n  );\n}\n\nexport { PermissionSelector, PermissionRow, PermissionOption };\nexport type {\n  PermissionSelectorProps,\n  PermissionRowProps,\n  PermissionOptionProps,\n  PermissionTone,\n};\n",
      "type": "registry:ui",
      "target": "@ui/permission-selector.tsx"
    }
  ],
  "type": "registry:ui"
}
