{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "route-summary",
  "title": "Route Summary",
  "description": "Travel modes, stops, totals, and turn-by-turn directions for a route.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "button",
    "toggle",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/route-summary.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  type ComponentProps,\n  createContext,\n  type KeyboardEvent,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const ROUTE_SUMMARY_VARIANTS = [\"card\", \"plain\", \"compact\"] as const;\nexport type RouteSummaryVariant = (typeof ROUTE_SUMMARY_VARIANTS)[number];\nexport const ROUTE_SUMMARY_STOP_KINDS = [\"origin\", \"stop\", \"destination\"] as const;\nexport type RouteSummaryStopKind = (typeof ROUTE_SUMMARY_STOP_KINDS)[number];\nexport const ROUTE_SUMMARY_ACTION_EMPHASES = [\"primary\", \"secondary\"] as const;\nexport type RouteSummaryActionEmphasis = (typeof ROUTE_SUMMARY_ACTION_EMPHASES)[number];\nexport type RouteSummaryProps = ComponentProps<\"section\"> & { variant?: RouteSummaryVariant };\n\ntype SummaryContext = { variant: RouteSummaryVariant };\nconst Context = createContext<SummaryContext | null>(null);\nfunction useSummary(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within RouteSummary`);\n  return context;\n}\n\nconst routeSummaryVariants = cva(\n  \"grid min-w-0 content-start text-[13px]/[18px] text-card-foreground\",\n  {\n    variants: {\n      variant: {\n        card: \"w-full max-w-sm gap-4 rounded-[14px] bg-card p-4 shadow-[0_0_0_1px_var(--border)]\",\n        plain: \"w-full max-w-sm gap-4 bg-transparent p-0\",\n        compact:\n          \"w-full max-w-xs gap-3 rounded-xl bg-card p-3 text-[12.5px]/[18px] shadow-[0_0_0_1px_var(--border)]\",\n      },\n    },\n  },\n);\n\n/** A route between places: travel mode, stops, totals, and turn-by-turn steps. */\nexport function RouteSummary({\n  variant = \"card\",\n  \"aria-label\": label = \"Route\",\n  className,\n  ...props\n}: RouteSummaryProps) {\n  return (\n    <Context.Provider value={{ variant }}>\n      <section\n        data-slot=\"route-summary\"\n        aria-label={label}\n        className={cn(routeSummaryVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      />\n    </Context.Provider>\n  );\n}\n\nexport type RouteSummaryModesProps = Omit<\n  ComponentProps<\"div\">,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"dir\"\n> & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\n\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\ntype ModesContextValue = { selected: string; select: (value: string) => void };\nconst ModesContext = createContext<ModesContextValue | null>(null);\n\nfunction useModes(part: string) {\n  const context = useContext(ModesContext);\n  if (!context) throw new Error(`${part} must be used within RouteSummaryModes`);\n  return context;\n}\n\n/** Arrows move focus and selection together, as in an APG radio group. */\nfunction moveRadio(event: KeyboardEvent<HTMLElement>) {\n  const radios = Array.from(\n    event.currentTarget.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]:not(:disabled)'),\n  );\n  const index = radios.indexOf(document.activeElement as HTMLButtonElement);\n  if (index === -1) return;\n  const next = {\n    ArrowRight: index + 1,\n    ArrowDown: index + 1,\n    ArrowLeft: index - 1,\n    ArrowUp: index - 1,\n    Home: 0,\n    End: radios.length - 1,\n  }[event.key];\n  if (next === undefined) return;\n  event.preventDefault();\n  const target = radios[(next + radios.length) % radios.length];\n  target?.focus();\n  target?.click();\n}\n\n/**\n * Single-choice segments are plain radio buttons styled like ToggleGroupItem: ToggleGroup's value\n * is a string on Radix but an array on Base UI, so the group's selection stays in uai state.\n */\nconst segmentClass = \"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10\";\n\n/** Travel modes. One is always selected; arrow keys move between them. */\nexport function RouteSummaryModes({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  \"aria-label\": label = \"Travel mode\",\n  onKeyDown,\n  className,\n  children,\n  ...props\n}: RouteSummaryModesProps) {\n  useSummary(\"RouteSummaryModes\");\n  const [internal, setInternal] = useState(defaultValue);\n  const selected = value ?? internal;\n  const select = (next: string) => {\n    // One mode always stays selected: pressing the selected mode again changes nothing.\n    if (next === selected) return;\n    if (value === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n  const ref = useRef<HTMLDivElement>(null);\n  const [thumb, setThumb] = useState<{ x: number; y: number; width: number; height: number }>();\n  useIsomorphicLayoutEffect(() => {\n    const list = ref.current;\n    if (!list) return;\n    const measure = () => {\n      const active = list.querySelector<HTMLElement>(\n        \"[data-slot=route-summary-mode][aria-checked=true]\",\n      );\n      if (!active || active.offsetWidth === 0) return setThumb(undefined);\n      setThumb({\n        x: active.offsetLeft,\n        y: active.offsetTop,\n        width: active.offsetWidth,\n        height: active.offsetHeight,\n      });\n    };\n    measure();\n    if (typeof ResizeObserver !== \"function\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(list);\n    return () => observer.disconnect();\n  }, [selected]);\n  return (\n    <ModesContext.Provider value={{ selected, select }}>\n      <div\n        role=\"radiogroup\"\n        data-slot=\"route-summary-modes\"\n        aria-label={label}\n        className={cn(\n          \"group/modes relative isolate flex w-full gap-1 rounded-full bg-muted p-1\",\n          className,\n        )}\n        {...props}\n        ref={ref}\n        data-ready={thumb ? \"\" : undefined}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (!event.defaultPrevented) moveRadio(event);\n        }}\n      >\n        {/* One measured thumb slides between modes; until it is measured the active mode paints itself. */}\n        <span\n          aria-hidden=\"true\"\n          data-slot=\"route-summary-modes-thumb\"\n          className=\"pointer-events-none absolute top-0 left-0 -z-1 rounded-full bg-card opacity-0 shadow-[0_1px_2px_oklch(0_0_0/0.12),0_0_0_1px_var(--border)] transition-[translate,width] duration-240 ease-out-quint group-data-ready/modes:opacity-100 motion-reduce:transition-none\"\n          style={{\n            width: thumb?.width ?? 0,\n            height: thumb?.height ?? 0,\n            translate: thumb ? `${thumb.x}px ${thumb.y}px` : undefined,\n          }}\n        />\n        {children}\n      </div>\n    </ModesContext.Provider>\n  );\n}\n\nexport type RouteSummaryModeProps = Omit<ComponentProps<\"button\">, \"value\"> & {\n  value: string;\n  /** Spoken name, for example \"Drive, 18 minutes\". */\n  label: string;\n};\n\n/** Pass the mode icon and its duration as children. */\nexport function RouteSummaryMode({\n  value,\n  label,\n  onClick,\n  className,\n  ...props\n}: RouteSummaryModeProps) {\n  const context = useSummary(\"RouteSummaryMode\");\n  const modes = useModes(\"RouteSummaryMode\");\n  const checked = modes.selected === value;\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a segment button in a radio group; ToggleGroupItem renders the same role=\"radio\" button.\n    <button\n      type=\"button\"\n      role=\"radio\"\n      aria-checked={checked}\n      data-slot=\"route-summary-mode\"\n      data-state={checked ? \"on\" : \"off\"}\n      aria-label={label}\n      title={label}\n      className={cn(\n        toggleVariants(),\n        segmentClass,\n        \"h-auto min-w-0 flex-1 gap-1.5 rounded-full! border-0 bg-transparent font-medium text-subtle-foreground tabular-nums shadow-none transition-[color,scale] duration-[120ms,140ms] ease-[ease-out,cubic-bezier(0.23,1,0.32,1)] hover:bg-transparent hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] data-[state=on]:bg-card data-[state=on]:text-foreground data-[state=on]:shadow-[0_1px_2px_oklch(0_0_0/0.12),0_0_0_1px_var(--border)] group-data-ready/modes:data-[state=on]:bg-transparent group-data-ready/modes:data-[state=on]:shadow-none motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:stroke-[1.75]\",\n        context.variant === \"compact\"\n          ? \"min-h-6 px-2 text-[11.5px]/4 [&_svg:not([class*='size-'])]:size-3.5\"\n          : \"min-h-7 px-2.5 text-[12px]/4 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n      tabIndex={checked || modes.selected === \"\" ? 0 : -1}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) modes.select(value);\n      }}\n    />\n  );\n}\n\n/** Totals for the selected mode. */\nexport function RouteSummaryOverview({ className, ...props }: ComponentProps<\"div\">) {\n  useSummary(\"RouteSummaryOverview\");\n  return (\n    <div\n      data-slot=\"route-summary-overview\"\n      className={cn(\"flex min-w-0 flex-wrap items-baseline gap-x-2.5 gap-y-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function RouteSummaryDuration({ className, ...props }: ComponentProps<\"p\">) {\n  const context = useSummary(\"RouteSummaryDuration\");\n  return (\n    <p\n      data-slot=\"route-summary-duration\"\n      className={cn(\n        \"m-0 font-semibold text-foreground tabular-nums tracking-[-0.015em]\",\n        context.variant === \"compact\" ? \"text-[17px]/[22px]\" : \"text-[22px]/7\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Distance, arrival time, or traffic, as muted text after the duration. */\nexport function RouteSummaryDetail({ className, ...props }: ComponentProps<\"p\">) {\n  useSummary(\"RouteSummaryDetail\");\n  return (\n    <p\n      data-slot=\"route-summary-detail\"\n      className={cn(\n        \"m-0 text-[12px]/4 text-muted-foreground tabular-nums data-[tone=warning]:text-warning-ink data-[tone=success]:text-success-ink\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function RouteSummaryStops({\n  \"aria-label\": label = \"Stops\",\n  className,\n  ...props\n}: ComponentProps<\"ol\">) {\n  useSummary(\"RouteSummaryStops\");\n  return (\n    <ol\n      data-slot=\"route-summary-stops\"\n      aria-label={label}\n      className={cn(\"m-0 grid list-none gap-0 p-0\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type RouteSummaryStopProps = ComponentProps<\"li\"> & { kind?: RouteSummaryStopKind };\n\n/** A waypoint on the rail. The first stop is the origin and the last is the destination. */\nexport function RouteSummaryStop({\n  kind = \"stop\",\n  className,\n  children,\n  ...props\n}: RouteSummaryStopProps) {\n  const context = useSummary(\"RouteSummaryStop\");\n  const compact = context.variant === \"compact\";\n  return (\n    <li\n      data-slot=\"route-summary-stop\"\n      data-kind={kind}\n      className={cn(\n        \"relative grid min-w-0 grid-cols-[16px_minmax(0,1fr)] items-center gap-x-2.5 text-foreground not-last:pb-3 not-last:after:absolute not-last:after:top-1/2 not-last:after:bottom-[calc(-50%+12px)] not-last:after:left-[7.5px] not-last:after:border-l-[1.5px] not-last:after:border-dotted not-last:after:border-border-strong\",\n        compact &&\n          \"not-last:pb-2 not-last:after:top-[calc(50%+2px)] not-last:after:bottom-[calc(-50%+10px)]\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"mx-auto rounded-full\",\n          kind === \"origin\" && \"size-2.5 shadow-[inset_0_0_0_2.5px_var(--foreground)]\",\n          kind === \"stop\" && \"size-2 bg-muted-foreground\",\n          kind === \"destination\" &&\n            \"size-3 bg-primary shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_22%,transparent)]\",\n        )}\n      />\n      <span className=\"min-w-0 truncate\">{children}</span>\n    </li>\n  );\n}\n\nexport type RouteSummaryStepsProps = ComponentProps<\"ol\">;\n\n/** Turn-by-turn directions, in order. */\nexport function RouteSummarySteps({\n  \"aria-label\": label = \"Directions\",\n  className,\n  ...props\n}: RouteSummaryStepsProps) {\n  useSummary(\"RouteSummarySteps\");\n  return (\n    <ol\n      data-slot=\"route-summary-steps\"\n      aria-label={label}\n      className={cn(\"m-0 grid list-none gap-0 border-t p-0 pt-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type RouteSummaryStepProps = ComponentProps<\"li\"> & {\n  /** Distance or time until the next step, for example \"0.3 mi\". */\n  distance?: string;\n};\n\n/** One maneuver. Pass a direction icon followed by the instruction. */\nexport function RouteSummaryStep({\n  distance,\n  className,\n  children,\n  ...props\n}: RouteSummaryStepProps) {\n  const context = useSummary(\"RouteSummaryStep\");\n  const id = useId();\n  return (\n    <li\n      data-slot=\"route-summary-step\"\n      aria-describedby={distance ? id : undefined}\n      className={cn(\n        \"flex min-w-0 items-center gap-2.5 border-b border-border/60 text-foreground last:border-b-0 [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:stroke-[1.75] [&>svg]:text-muted-foreground\",\n        context.variant === \"compact\" ? \"min-h-8 py-1\" : \"min-h-10 py-1.5\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {distance && (\n        <span\n          id={id}\n          className=\"ml-auto shrink-0 pl-2 text-[11.5px]/4 text-subtle-foreground tabular-nums\"\n        >\n          {distance}\n        </span>\n      )}\n    </li>\n  );\n}\n\nexport function RouteSummaryActions({ className, ...props }: ComponentProps<\"div\">) {\n  useSummary(\"RouteSummaryActions\");\n  return (\n    <div\n      data-slot=\"route-summary-actions\"\n      className={cn(\"flex min-w-0 flex-wrap items-center gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nconst routeSummaryActionVariants = cva(\n  \"h-auto gap-1.5 rounded-full border-0 py-0 shadow-none transition-[scale,background-color,filter] duration-140 ease-out-quint focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    variants: {\n      variant: {\n        card: \"min-h-8 px-3.5 text-[13px]/4 has-[>svg]:px-3\",\n        plain: \"min-h-8 px-3.5 text-[13px]/4 has-[>svg]:px-3\",\n        compact: \"min-h-7 px-3 text-[12px]/4 has-[>svg]:px-2.5\",\n      },\n      emphasis: {\n        primary: \"bg-primary text-primary-foreground hover:bg-primary hover:brightness-[1.08]\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))]\",\n      },\n    },\n  },\n);\n\nexport type RouteSummaryActionProps = ComponentProps<\"button\"> & {\n  emphasis?: RouteSummaryActionEmphasis;\n};\n\n/** Use the primary emphasis once, usually for Start. */\nexport function RouteSummaryAction({\n  emphasis = \"secondary\",\n  type = \"button\",\n  className,\n  ...props\n}: RouteSummaryActionProps) {\n  const context = useSummary(\"RouteSummaryAction\");\n  return (\n    <Button\n      data-slot=\"route-summary-action\"\n      type={type}\n      variant={emphasis === \"primary\" ? \"default\" : \"secondary\"}\n      className={cn(routeSummaryActionVariants({ variant: context.variant, emphasis }), className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/route-summary.tsx"
    }
  ],
  "categories": [
    "maps",
    "navigation"
  ],
  "type": "registry:ui"
}