{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flag-board",
  "type": "registry:component",
  "title": "Flag Board",
  "description": "Feature flags per environment: flip a flag, drag its rollout and see how many people that is, target segments with chips, and a kill switch you have to press and hold, with undo, before it turns everything off.",
  "categories": [
    "component",
    "Dev Tools"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/menu.json",
    "https://coss.com/ui/r/slider.json",
    "https://coss.com/ui/r/switch.json",
    "https://coss.com/ui/r/tabs.json"
  ],
  "files": [
    {
      "path": "registry/devl/flag-board.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { PlusIcon, PowerIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { type KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Menu, MenuItem, MenuPopup, MenuTrigger } from \"@/components/ui/menu\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tabs, TabsList, TabsTab } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface FlagState {\n  on: boolean;\n  rollout: number;\n  segments: string[];\n}\n\nexport interface FeatureFlag {\n  key: string;\n  description: string;\n  envs: Record<string, FlagState>;\n  updated?: string;\n}\n\nexport interface FlagEnvironment {\n  id: string;\n  name: string;\n  audience?: number;\n}\n\nexport interface FlagSegment {\n  id: string;\n  name: string;\n}\n\nexport interface FlagBoardProps {\n  flags: FeatureFlag[];\n  environments: FlagEnvironment[];\n  segments: FlagSegment[];\n  defaultEnvironment?: string;\n  onChange?: (flags: FeatureFlag[]) => void;\n  className?: string;\n}\n\nconst HOLD_MS = 1100;\n\nfunction HoldToConfirm({ label, onConfirm }: { label: string; onConfirm: () => void }) {\n  const reduce = useReducedMotion();\n  const progress = useMotionValue(0);\n  const width = useTransform(progress, (p) => `${p * 100}%`);\n  const clip = useTransform(progress, (p) => `inset(0 ${100 - p * 100}% 0 0)`);\n  const run = useRef<ReturnType<typeof animate> | null>(null);\n  const [holding, setHolding] = useState(false);\n\n  const start = () => {\n    setHolding(true);\n    run.current?.stop();\n    run.current = animate(progress, 1, {\n      duration: (HOLD_MS / 1000) * (1 - progress.get()),\n      ease: \"linear\",\n      onComplete: () => {\n        setHolding(false);\n        onConfirm();\n      },\n    });\n  };\n  const cancel = () => {\n    if (progress.get() >= 1) return;\n    setHolding(false);\n    run.current?.stop();\n    run.current = animate(progress, 0, { duration: reduce ? 0 : 0.25, ease: [0.23, 1, 0.32, 1] });\n  };\n  useEffect(() => () => run.current?.stop(), []);\n\n  const onKeyDown = (e: KeyboardEvent) => {\n    if ((e.key === \" \" || e.key === \"Enter\") && !e.repeat) {\n      e.preventDefault();\n      start();\n    }\n  };\n  const onKeyUp = (e: KeyboardEvent) => {\n    if (e.key === \" \" || e.key === \"Enter\") cancel();\n  };\n\n  return (\n    <button\n      aria-label={`${label}. Press and hold to confirm.`}\n      className=\"relative isolate inline-flex h-8 w-[148px] shrink-0 select-none items-center justify-center gap-1.5 overflow-hidden rounded-lg border border-destructive bg-destructive/10 px-3 font-medium text-destructive-foreground text-sm outline-none transition-[scale] duration-150 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background active:scale-[0.97]\"\n      onBlur={cancel}\n      onContextMenu={(e) => e.preventDefault()}\n      onKeyDown={onKeyDown}\n      onKeyUp={onKeyUp}\n      onPointerCancel={cancel}\n      onPointerDown={start}\n      onPointerLeave={cancel}\n      onPointerUp={cancel}\n      type=\"button\"\n    >\n      <motion.span aria-hidden className=\"absolute inset-y-0 left-0 -z-10 bg-destructive\" style={{ width }} />\n      <motion.span\n        aria-hidden\n        className=\"absolute inset-0 -z-0 flex items-center justify-center gap-1.5 text-white\"\n        style={{ clipPath: clip }}\n      >\n        <PowerIcon className=\"size-3.5\" />\n        {holding ? \"Keep holding…\" : label}\n      </motion.span>\n      <PowerIcon className=\"size-3.5\" />\n      {holding ? \"Keep holding…\" : label}\n    </button>\n  );\n}\n\nfunction compact(n: number) {\n  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n  if (n >= 10_000) return `${Math.round(n / 1000)}k`;\n  if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;\n  return String(n);\n}\n\nexport function FlagBoard({ flags: initial, environments, segments, defaultEnvironment, onChange, className }: FlagBoardProps) {\n  const reduce = useReducedMotion();\n  const [flags, setFlags] = useState(initial);\n  const [env, setEnv] = useState(defaultEnvironment ?? environments[0]?.id);\n  const [confirming, setConfirming] = useState(false);\n  const [killed, setKilled] = useState<{ env: string; keys: string[] } | null>(null);\n  const mounted = useRef(false);\n  useEffect(() => {\n    mounted.current = true;\n  }, []);\n\n  const environment = environments.find((e) => e.id === env) ?? environments[0];\n  const segName = (id: string) => segments.find((s) => s.id === id)?.name ?? id;\n  const live = flags.filter((f) => f.envs[env]?.on);\n\n  useEffect(() => {\n    if (!killed) return;\n    const t = setTimeout(() => setKilled(null), 6000);\n    return () => clearTimeout(t);\n  }, [killed]);\n\n  const update = (next: FeatureFlag[]) => {\n    setFlags(next);\n    onChange?.(next);\n  };\n\n  const patch = (key: string, p: Partial<FlagState>) =>\n    update(flags.map((f) => (f.key === key ? { ...f, updated: \"just now\", envs: { ...f.envs, [env]: { ...f.envs[env], ...p } } } : f)));\n\n  const killAll = () => {\n    setKilled({ env, keys: live.map((f) => f.key) });\n    update(flags.map((f) => (f.envs[env]?.on ? { ...f, updated: \"just now\", envs: { ...f.envs, [env]: { ...f.envs[env], on: false } } } : f)));\n    setConfirming(false);\n  };\n\n  const undoKill = () => {\n    if (!killed) return;\n    update(flags.map((f) => (killed.keys.includes(f.key) ? { ...f, envs: { ...f.envs, [killed.env]: { ...f.envs[killed.env], on: true } } } : f)));\n    setKilled(null);\n  };\n\n  return (\n    <div className={cn(\"flex w-full min-w-0 max-w-[560px] flex-col overflow-hidden rounded-2xl border bg-card text-sm shadow-xs/5\", className)}>\n      <div className=\"flex items-center gap-3 px-4 pt-3.5 pb-3\">\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"font-medium\">Feature flags</h3>\n          <p className=\"text-muted-foreground text-xs tabular-nums\">\n            {live.length} of {flags.length} on in {environment.name}\n          </p>\n        </div>\n        <Button\n          aria-expanded={confirming}\n          className=\"shrink-0\"\n          disabled={live.length === 0}\n          onClick={() => setConfirming((c) => !c)}\n          size=\"sm\"\n          variant=\"destructive-outline\"\n        >\n          <PowerIcon />\n          Kill switch\n        </Button>\n      </div>\n\n      <div className=\"px-4 pb-3\">\n        <Tabs\n          onValueChange={(v) => {\n            setEnv(String(v));\n            setConfirming(false);\n          }}\n          value={env}\n        >\n          <TabsList className=\"w-full text-muted-foreground\">\n            {environments.map((e) => {\n              const on = flags.filter((f) => f.envs[e.id]?.on).length;\n              return (\n                <TabsTab className=\"gap-1.5 text-xs sm:text-xs\" key={e.id} value={e.id}>\n                  {e.name}\n                  <span className=\"rounded-full bg-foreground/6 px-1.5 text-[10.5px] text-muted-foreground tabular-nums leading-4 max-sm:hidden dark:bg-foreground/10\">\n                    {on}\n                  </span>\n                </TabsTab>\n              );\n            })}\n          </TabsList>\n        </Tabs>\n      </div>\n\n      <AnimatePresence initial={false}>\n        {confirming && (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={{ height: 0, opacity: 0 }}\n            initial={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : { duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <div className=\"mx-4 mb-3 flex flex-col gap-3 rounded-xl border border-destructive/24 bg-destructive/6 p-3 sm:flex-row sm:items-center\">\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"font-medium text-destructive-foreground text-sm\">\n                  Turn off {live.length} {live.length === 1 ? \"flag\" : \"flags\"} in {environment.name}?\n                </p>\n                <p className=\"mt-0.5 text-muted-foreground text-xs text-pretty\">\n                  {environment.audience ? `${compact(environment.audience)} users` : \"Everyone\"} fall back to the default experience right away.\n                </p>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <Button onClick={() => setConfirming(false)} size=\"sm\" variant=\"ghost\">\n                  Cancel\n                </Button>\n                <HoldToConfirm label=\"Hold to turn off\" onConfirm={killAll} />\n              </div>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <AnimatePresence initial={false}>\n        {killed && killed.env === env && !confirming && (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={{ height: 0, opacity: 0 }}\n            initial={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : { duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <div className=\"mx-4 mb-3 flex items-center gap-2 rounded-xl border bg-muted/50 py-1.5 ps-3 pe-1.5 text-xs\">\n              <PowerIcon className=\"size-3.5 text-muted-foreground\" />\n              <span className=\"min-w-0 flex-1 truncate\">\n                {killed.keys.length} {killed.keys.length === 1 ? \"flag\" : \"flags\"} turned off in {environment.name}\n              </span>\n              <Button className=\"text-xs sm:text-xs\" onClick={undoKill} size=\"xs\" variant=\"ghost\">\n                Undo\n              </Button>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <motion.ul\n        animate={{ opacity: 1 }}\n        className=\"border-t\"\n        initial={mounted.current ? { opacity: 0.4 } : false}\n        key={env}\n        transition={{ duration: 0.18 }}\n      >\n        {flags.map((f) => {\n          const s = f.envs[env] ?? { on: false, rollout: 0, segments: [] };\n          const available = segments.filter((seg) => !s.segments.includes(seg.id));\n          const reach = environment.audience ? Math.round((environment.audience * s.rollout) / 100) : null;\n          return (\n            <li className=\"border-b px-4 py-3 last:border-b-0\" key={f.key}>\n              <div className=\"flex items-start gap-3\">\n                <Switch aria-label={`${f.key} in ${environment.name}`} checked={s.on} className=\"mt-0.5\" onCheckedChange={(on) => patch(f.key, { on })} />\n                <div className=\"min-w-0 flex-1\">\n                  <div className=\"flex items-baseline gap-2\">\n                    <code className={cn(\"truncate font-medium font-mono text-[13px] transition-colors\", !s.on && \"text-muted-foreground\")}>{f.key}</code>\n                    <span className=\"ms-auto shrink-0 text-muted-foreground text-xs\">{f.updated}</span>\n                  </div>\n                  <p className=\"mt-0.5 text-muted-foreground text-xs text-pretty\">{f.description}</p>\n\n                  <AnimatePresence initial={false}>\n                    {s.on && (\n                      <motion.div\n                        animate={{ height: \"auto\", opacity: 1 }}\n                        className=\"overflow-hidden\"\n                        exit={{ height: 0, opacity: 0 }}\n                        initial={{ height: 0, opacity: 0 }}\n                        transition={reduce ? { duration: 0 } : { duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n                      >\n                        <div className=\"flex flex-col gap-2.5 pt-3\">\n                          <div className=\"flex items-center gap-3\">\n                            <Slider\n                              getAriaLabel={() => `${f.key} rollout`}\n                              className=\"min-w-0 flex-1 [&_[data-slot=slider-control]]:min-w-0\"\n                              onValueChange={(v) => patch(f.key, { rollout: Array.isArray(v) ? v[0] : v })}\n                              step={5}\n                              value={[s.rollout]}\n                            />\n                            <span className=\"w-[78px] shrink-0 whitespace-nowrap text-end text-xs tabular-nums\">\n                              <span className=\"font-medium\">{s.rollout}%</span>\n                              {reach !== null && (\n                                <span className=\"text-muted-foreground\">\n                                  {\" \"}\n                                  · {compact(reach)}\n                                  <span className=\"sr-only\"> users</span>\n                                </span>\n                              )}\n                            </span>\n                          </div>\n                          <div className=\"flex flex-wrap items-center gap-1.5\">\n                            <span className=\"me-0.5 text-muted-foreground text-xs\">{s.segments.length ? \"Only\" : \"Targeting\"}</span>\n                            <AnimatePresence initial={false} mode=\"popLayout\">\n                              {s.segments.length === 0 && (\n                                <motion.span\n                                  animate={{ opacity: 1, scale: 1 }}\n                                  className=\"inline-flex h-6 items-center rounded-full border border-dashed px-2 text-muted-foreground text-xs\"\n                                  exit={{ opacity: 0, scale: 0.95 }}\n                                  initial={{ opacity: 0, scale: 0.95 }}\n                                  key=\"all\"\n                                  transition={{ duration: 0.15 }}\n                                >\n                                  everyone\n                                </motion.span>\n                              )}\n                              {s.segments.map((id) => (\n                                <motion.span\n                                  animate={{ opacity: 1, scale: 1 }}\n                                  className=\"inline-flex h-6 items-center gap-0.5 rounded-full bg-info/10 ps-2 pe-0.5 text-info-foreground text-xs\"\n                                  exit={{ opacity: 0, scale: 0.95 }}\n                                  initial={reduce ? false : { opacity: 0, scale: 0.95 }}\n                                  key={id}\n                                  layout={reduce ? false : \"position\"}\n                                  transition={{ type: \"spring\", stiffness: 500, damping: 36 }}\n                                >\n                                  {segName(id)}\n                                  <button\n                                    aria-label={`Remove ${segName(id)}`}\n                                    className=\"relative grid size-5 place-items-center rounded-full outline-none transition-colors after:absolute after:-inset-1.5 focus-visible:ring-2 focus-visible:ring-ring [@media(hover:hover)]:hover:bg-info/15\"\n                                    onClick={() => patch(f.key, { segments: s.segments.filter((x) => x !== id) })}\n                                    type=\"button\"\n                                  >\n                                    <XIcon className=\"size-3\" />\n                                  </button>\n                                </motion.span>\n                              ))}\n                            </AnimatePresence>\n                            {available.length > 0 && (\n                              <Menu>\n                                <MenuTrigger\n                                  aria-label={`Add segment to ${f.key}`}\n                                  className=\"relative grid size-6 place-items-center rounded-full border border-dashed text-muted-foreground outline-none transition-colors after:absolute after:-inset-1 focus-visible:ring-2 focus-visible:ring-ring data-popup-open:border-solid data-popup-open:text-foreground [@media(hover:hover)]:hover:border-solid [@media(hover:hover)]:hover:text-foreground\"\n                                >\n                                  <PlusIcon className=\"size-3.5\" />\n                                </MenuTrigger>\n                                <MenuPopup align=\"start\">\n                                  {available.map((seg) => (\n                                    <MenuItem key={seg.id} onClick={() => patch(f.key, { segments: [...s.segments, seg.id] })}>\n                                      {seg.name}\n                                    </MenuItem>\n                                  ))}\n                                </MenuPopup>\n                              </Menu>\n                            )}\n                          </div>\n                        </div>\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </div>\n            </li>\n          );\n        })}\n      </motion.ul>\n    </div>\n  );\n}\n"
    }
  ]
}