diff --git a/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/PersistedModelAnchorTimeWidget.styled.tsx b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/PersistedModelAnchorTimeWidget.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4031e3a7cfde3f4fea0322550887b5946a4b161a
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/PersistedModelAnchorTimeWidget.styled.tsx
@@ -0,0 +1,12 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+
+export const RelativeTimeLabel = styled.span`
+  display: block;
+
+  color: ${color("text-medium")};
+  font-size: 0.875rem;
+  font-weight: 400;
+
+  margin-top: 1.5rem;
+`;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/PersistedModelAnchorTimeWidget.tsx b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/PersistedModelAnchorTimeWidget.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..82cfafd5678d758f2b4c5f5656f6def3f710f8a7
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/PersistedModelAnchorTimeWidget.tsx
@@ -0,0 +1,93 @@
+import React, { useCallback, useMemo } from "react";
+import _ from "underscore";
+import moment, { Moment } from "moment";
+import { t } from "ttag";
+
+import TimeInput from "metabase/core/components/TimeInput";
+
+import { has24HourModeSetting } from "metabase/lib/time";
+
+import { formatTime, getNextExpectedRefreshTime } from "./utils";
+import { RelativeTimeLabel } from "./PersistedModelAnchorTimeWidget.styled";
+
+const DEFAULT_REFRESH_INTERVAL = 6;
+
+type Props = {
+  setting: {
+    key: string;
+    value: string | null;
+    default: string;
+  };
+  settingValues: {
+    "persisted-model-refresh-interval-hours": number | null;
+  };
+  onChange: (anchor: string) => void;
+};
+
+function PersistedModelAnchorTimeWidget({
+  setting,
+  onChange,
+  settingValues,
+}: Props) {
+  const anchor = setting.value || setting.default;
+  const [hours, minutes] = anchor.split(":").map(value => parseInt(value, 10));
+  const value = moment({ hours, minutes });
+
+  const onChangeDebounced = useMemo(() => _.debounce(onChange, 300), [
+    onChange,
+  ]);
+
+  const handleChange = useCallback(
+    (value: Moment) => {
+      const hours = value.hours();
+      const minutes = value.minutes();
+      const nextAnchor = `${formatTime(hours)}:${formatTime(minutes)}`;
+      onChangeDebounced(nextAnchor);
+    },
+    [onChangeDebounced],
+  );
+
+  const nextRefreshDates = useMemo(() => {
+    const refreshInterval =
+      settingValues["persisted-model-refresh-interval-hours"] ||
+      DEFAULT_REFRESH_INTERVAL;
+    const firstRefresh = getNextExpectedRefreshTime(
+      moment(),
+      refreshInterval,
+      anchor,
+    );
+    const secondRefresh = getNextExpectedRefreshTime(
+      firstRefresh,
+      refreshInterval,
+      anchor,
+    );
+    const thirdRefresh = getNextExpectedRefreshTime(
+      secondRefresh,
+      refreshInterval,
+      anchor,
+    );
+    return [firstRefresh, secondRefresh, thirdRefresh];
+  }, [anchor, settingValues]);
+
+  const renderRefreshTimeHintText = useCallback(() => {
+    const [first, second, third] = nextRefreshDates.map(time =>
+      time.fromNow(true),
+    );
+    return (
+      <RelativeTimeLabel>{t`The next three refresh jobs will run in ${first}, ${second}, and ${third}.`}</RelativeTimeLabel>
+    );
+  }, [nextRefreshDates]);
+
+  return (
+    <div>
+      <TimeInput.Compact
+        value={value}
+        is24HourMode={has24HourModeSetting()}
+        onChange={handleChange}
+      />
+      {renderRefreshTimeHintText()}
+    </div>
+  );
+}
+
+export default PersistedModelAnchorTimeWidget;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/index.ts b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8fd1efa58d05d79f406d18a535040445f6be86da
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/index.ts
@@ -0,0 +1 @@
+export { default } from "./PersistedModelAnchorTimeWidget";
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/utils.ts b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b40ca88b143259275cef6f17e050323e43c26aca
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/utils.ts
@@ -0,0 +1,25 @@
+import { Moment } from "moment";
+
+export function formatTime(value: number) {
+  return value < 10 ? `0${value}` : value;
+}
+
+export function getNextExpectedRefreshTime(
+  fromTime: Moment,
+  refreshInterval: number,
+  anchorTime: string,
+) {
+  const nextRefresh = fromTime.clone();
+  nextRefresh.add(refreshInterval, "hours");
+
+  const isNextDay = nextRefresh.date() !== fromTime.date();
+  if (isNextDay) {
+    const [hours, minutes] = anchorTime
+      .split(":")
+      .map(number => parseInt(number, 10));
+    nextRefresh.hours(hours);
+    nextRefresh.minutes(minutes);
+  }
+
+  return nextRefresh;
+}
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/utils.unit.spec.ts b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/utils.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f63701b0241c30cdd3543dfc210ebcb7cc2cb813
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/PersistedModelAnchorTimeWidget/utils.unit.spec.ts
@@ -0,0 +1,25 @@
+import moment, { Moment } from "moment";
+import { getNextExpectedRefreshTime } from "./utils";
+
+describe("getNextExpectedRefreshTime", () => {
+  it("should jump forward by refresh interval (hours)", () => {
+    const fromTime = moment({ hours: 12, minutes: 0 });
+    const result = getNextExpectedRefreshTime(fromTime, 8, "10:00");
+    expect(result.hours()).toBe(20);
+    expect(result.minutes()).toBe(0);
+  });
+
+  it("should respect minutes", () => {
+    const fromTime = moment({ hours: 12, minutes: 15 });
+    const result = getNextExpectedRefreshTime(fromTime, 6, "10:00");
+    expect(result.hours()).toBe(18);
+    expect(result.minutes()).toBe(15);
+  });
+
+  it("should stick to anchor time for refreshes tomorrow", () => {
+    const fromTime = moment({ hours: 19, minutes: 0 });
+    const result = getNextExpectedRefreshTime(fromTime, 6, "10:00");
+    expect(result.hours()).toBe(10);
+    expect(result.minutes()).toBe(0);
+  });
+});
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index 741ae4ab36d79a0e2e755f735d8c86e023b8ac02..6bfbd1d854c4851cf6cf0420d5cd660b1814ce38 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -20,6 +20,7 @@ import SecretKeyWidget from "./components/widgets/SecretKeyWidget";
 import EmbeddingLegalese from "./components/widgets/EmbeddingLegalese";
 import FormattingWidget from "./components/widgets/FormattingWidget";
 import { PremiumEmbeddingLinkWidget } from "./components/widgets/PremiumEmbeddingLinkWidget";
+import PersistedModelAnchorTimeWidget from "./components/widgets/PersistedModelAnchorTimeWidget";
 import PersistedModelRefreshIntervalWidget from "./components/widgets/PersistedModelRefreshIntervalWidget";
 import SectionDivider from "./components/widgets/SectionDivider";
 import SettingsUpdatesForm from "./components/SettingsUpdatesForm/SettingsUpdatesForm";
@@ -54,6 +55,8 @@ function updateSectionsWithPlugins(sections) {
   }
 }
 
+const CACHING_MIN_REFRESH_HOURS_FOR_ANCHOR_TIME_SETTING = 6;
+
 const SECTIONS = updateSectionsWithPlugins({
   setup: {
     name: t`Setup`,
@@ -449,8 +452,28 @@ const SECTIONS = updateSectionsWithPlugins({
         disableDefaultUpdate: true,
         widget: PersistedModelRefreshIntervalWidget,
         getHidden: settings => !settings["persisted-models-enabled"],
-        onChanged: async (oldValue, newValue) =>
-          PersistedModelsApi.setRefreshInterval({ hours: newValue }),
+        onChanged: (oldHours, hours) =>
+          PersistedModelsApi.setRefreshInterval({ hours }),
+      },
+      {
+        key: "persisted-model-refresh-anchor-time",
+        display_name: t`Anchoring time`,
+        disableDefaultUpdate: true,
+        widget: PersistedModelAnchorTimeWidget,
+        getHidden: settings => {
+          if (!settings["persisted-models-enabled"]) {
+            return true;
+          }
+          const DEFAULT_REFRESH_INTERVAL = 6;
+          const refreshInterval =
+            settings["persisted-model-refresh-interval-hours"] ||
+            DEFAULT_REFRESH_INTERVAL;
+          return (
+            refreshInterval < CACHING_MIN_REFRESH_HOURS_FOR_ANCHOR_TIME_SETTING
+          );
+        },
+        onChanged: (oldAnchor, anchor) =>
+          PersistedModelsApi.setRefreshInterval({ anchor }),
       },
     ],
   },
diff --git a/frontend/src/metabase/components/SegmentedControl.info.js b/frontend/src/metabase/components/SegmentedControl.info.js
index b4e59cc97d732e93e25d35693da01c96550063e9..a23dec4be10a988ae58a46be2c48cfbc5727d0a1 100644
--- a/frontend/src/metabase/components/SegmentedControl.info.js
+++ b/frontend/src/metabase/components/SegmentedControl.info.js
@@ -57,6 +57,7 @@ export const examples = {
         options={SIMPLE_OPTIONS}
         variant="fill-background"
       />
+      <SegmentedControlDemo options={SIMPLE_OPTIONS} variant="fill-all" />
     </React.Fragment>
   ),
   icons: <SegmentedControlDemo options={OPTIONS_WITH_ICONS} />,
diff --git a/frontend/src/metabase/components/SegmentedControl.jsx b/frontend/src/metabase/components/SegmentedControl.jsx
index 0a9370e5148972e06dd65895249d0b36e4c1a2c7..cca132cb22af0b4afda815fc7ac8cd617e2bd54c 100644
--- a/frontend/src/metabase/components/SegmentedControl.jsx
+++ b/frontend/src/metabase/components/SegmentedControl.jsx
@@ -25,7 +25,7 @@ const propTypes = {
   name: PropTypes.string,
   value: PropTypes.any,
   options: PropTypes.arrayOf(optionShape).isRequired,
-  variant: PropTypes.oneOf(["fill-text", "fill-background"]),
+  variant: PropTypes.oneOf(["fill-text", "fill-background", "fill-all"]),
   inactiveColor: PropTypes.string,
   onChange: PropTypes.func,
   fullWidth: PropTypes.bool,
@@ -45,12 +45,13 @@ export function SegmentedControl({
 }) {
   const id = useMemo(() => _.uniqueId("radio-"), []);
   const name = nameFromProps || id;
+  const selectedOptionIndex = options.findIndex(
+    option => option.value === value,
+  );
   return (
     <SegmentedList {...props} fullWidth={fullWidth}>
       {options.map((option, index) => {
-        const isSelected = option.value === value;
-        const isFirst = index === 0;
-        const isLast = index === options.length - 1;
+        const isSelected = index === selectedOptionIndex;
         const id = `${name}-${option.value}`;
         const labelId = `${name}-${option.value}`;
         const iconOnly = !option.name;
@@ -59,8 +60,9 @@ export function SegmentedControl({
           <SegmentedItem
             key={option.value}
             isSelected={isSelected}
-            isFirst={isFirst}
-            isLast={isLast}
+            index={index}
+            total={options.length}
+            selectedOptionIndex={selectedOptionIndex}
             fullWidth={fullWidth}
             variant={variant}
             selectedColor={selectedColor}
diff --git a/frontend/src/metabase/components/SegmentedControl.styled.jsx b/frontend/src/metabase/components/SegmentedControl.styled.jsx
index f9ac65ce4e9caad98e17a8e2c82e157c7fb9dffd..2afb5f3420307771b96de29bdc13b72666b7a5d2 100644
--- a/frontend/src/metabase/components/SegmentedControl.styled.jsx
+++ b/frontend/src/metabase/components/SegmentedControl.styled.jsx
@@ -1,37 +1,90 @@
 import React from "react";
 import styled from "@emotion/styled";
+import { css } from "@emotion/react";
 import _ from "underscore";
 import Icon from "metabase/components/Icon";
-import { color, darken } from "metabase/lib/colors";
+import { color, darken, alpha } from "metabase/lib/colors";
 
-const BORDER_RADIUS = "8px";
+function getDefaultBorderColor() {
+  return darken(color("border"), 0.1);
+}
 
-export const SegmentedList = styled.ul`
-  display: flex;
-  width: ${props => (props.fullWidth ? 1 : 0)};
-`;
+const COLORS = {
+  "fill-text": {
+    background: () => "transparent",
+    border: () => getDefaultBorderColor(),
+    text: ({ isSelected, selectedColor, inactiveColor }) =>
+      color(isSelected ? selectedColor : inactiveColor),
+  },
+  "fill-background": {
+    background: ({ isSelected, selectedColor }) =>
+      isSelected ? color(selectedColor) : "transparent",
+    border: ({ isSelected, selectedColor }) =>
+      isSelected ? color(selectedColor) : getDefaultBorderColor(),
+    text: ({ isSelected, inactiveColor }) =>
+      color(isSelected ? "text-white" : inactiveColor),
+  },
+  "fill-all": {
+    background: ({ isSelected, selectedColor }) =>
+      isSelected ? alpha(color(selectedColor), 0.2) : "transparent",
+    border: ({ isSelected, selectedColor }) =>
+      isSelected ? color(selectedColor) : getDefaultBorderColor(),
+    text: ({ isSelected, selectedColor, inactiveColor }) =>
+      color(isSelected ? selectedColor : inactiveColor),
+  },
+};
+
+function getSpecialBorderStyles({
+  index,
+  isSelected,
+  total,
+  selectedOptionIndex,
+}) {
+  if (isSelected) {
+    return css`
+      border-right-width: 1px;
+      border-left-width: 1px;
+    `;
+  }
+
+  const isBeforeSelected = index === selectedOptionIndex - 1;
+  if (isBeforeSelected) {
+    return css`
+      border-right-width: 0;
+    `;
+  }
+
+  const isAfterSelected = index === selectedOptionIndex + 1;
+  if (isAfterSelected) {
+    return css`
+      border-left-width: 0;
+    `;
+  }
 
-function getSegmentedItemColor(props, fallbackColor) {
-  if (props.variant === "fill-text") {
-    return fallbackColor;
+  const isFirst = index === 0;
+  if (isFirst) {
+    return css`
+      border-left-width: 1px;
+      border-right-width: 0;
+    `;
+  }
+  const isLast = index === total - 1;
+  if (isLast) {
+    return css`
+      border-right-width: 1px;
+      border-left-width: 0;
+    `;
   }
-  return props.isSelected ? color(props.selectedColor) : fallbackColor;
 }
 
 export const SegmentedItem = styled.li`
   display: flex;
   flex-grow: ${props => (props.fullWidth ? 1 : 0)};
 
-  background-color: ${props => getSegmentedItemColor(props, "transparent")};
-
-  border: 1px solid
-    ${props => getSegmentedItemColor(props, darken(color("border"), 0.1))};
+  background-color: ${props => COLORS[props.variant].background(props)};
+  border: 1px solid ${props => COLORS[props.variant].border(props)};
 
-  border-right-width: ${props => (props.isLast ? "1px" : 0)};
-  border-top-left-radius: ${props => (props.isFirst ? BORDER_RADIUS : 0)};
-  border-bottom-left-radius: ${props => (props.isFirst ? BORDER_RADIUS : 0)};
-  border-top-right-radius: ${props => (props.isLast ? BORDER_RADIUS : 0)};
-  border-bottom-right-radius: ${props => (props.isLast ? BORDER_RADIUS : 0)};
+  ${props => getSpecialBorderStyles(props)};
 `;
 
 export const SegmentedItemLabel = styled.label`
@@ -41,12 +94,7 @@ export const SegmentedItemLabel = styled.label`
   justify-content: center;
   position: relative;
   font-weight: bold;
-  color: ${props => {
-    const selectedColor = color(
-      props.variant === "fill-text" ? props.selectedColor : "white",
-    );
-    return props.isSelected ? selectedColor : color(props.inactiveColor);
-  }};
+  color: ${props => COLORS[props.variant].text(props)};
   padding: ${props => (props.compact ? "8px" : "8px 12px")};
   cursor: pointer;
 
@@ -77,3 +125,21 @@ function IconWrapper(props) {
 export const ItemIcon = styled(IconWrapper)`
   margin-right: ${props => (props.iconOnly ? 0 : "4px")};
 `;
+
+const BORDER_RADIUS = "8px";
+
+export const SegmentedList = styled.ul`
+  display: flex;
+
+  ${SegmentedItem} {
+    &:first-of-type {
+      border-top-left-radius: ${BORDER_RADIUS};
+      border-bottom-left-radius: ${BORDER_RADIUS};
+    }
+
+    &:last-of-type {
+      border-top-right-radius: ${BORDER_RADIUS};
+      border-bottom-right-radius: ${BORDER_RADIUS};
+    }
+  }
+`;
diff --git a/frontend/src/metabase/core/components/Input/Input.tsx b/frontend/src/metabase/core/components/Input/Input.tsx
index 86eb0595ed3540fa212c08edcb0f1e771e2aea7d..989d614dde72a5456d1068eefe2cf3190ef027c0 100644
--- a/frontend/src/metabase/core/components/Input/Input.tsx
+++ b/frontend/src/metabase/core/components/Input/Input.tsx
@@ -84,4 +84,7 @@ const Input = forwardRef(function Input(
   );
 });
 
-export default Input;
+export default Object.assign(Input, {
+  Root: InputRoot,
+  Field: InputField,
+});
diff --git a/frontend/src/metabase/core/components/TimeInput/CompactTimeInput.styled.tsx b/frontend/src/metabase/core/components/TimeInput/CompactTimeInput.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6c4ac3c91c4c910fe9ab50d501f3fc9f05b03d9b
--- /dev/null
+++ b/frontend/src/metabase/core/components/TimeInput/CompactTimeInput.styled.tsx
@@ -0,0 +1,27 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+import NumericInput from "metabase/core/components/NumericInput";
+import Input from "metabase/core/components/Input";
+
+export const CompactInputContainer = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 0.55rem 1rem;
+
+  border-radius: 8px;
+  border: 1px solid ${color("border")};
+  background-color: ${color("white")};
+`;
+
+export const CompactInput = styled(NumericInput)`
+  width: 1rem;
+  text-align: center;
+
+  ${Input.Root}, ${Input.Field} {
+    border: none;
+    padding: 0;
+    margin: 0;
+
+    font-size: 0.875rem;
+  }
+`;
diff --git a/frontend/src/metabase/core/components/TimeInput/CompactTimeInput.tsx b/frontend/src/metabase/core/components/TimeInput/CompactTimeInput.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..74f398322cca7f1dfa794a96fd73373ace2b092c
--- /dev/null
+++ b/frontend/src/metabase/core/components/TimeInput/CompactTimeInput.tsx
@@ -0,0 +1,81 @@
+import React, { forwardRef, Ref, useCallback } from "react";
+import { t } from "ttag";
+
+import { SegmentedControl } from "metabase/components/SegmentedControl";
+
+import useTimeInput, { BaseTimeInputProps } from "./useTimeInput";
+import {
+  InputDivider,
+  InputRoot,
+  InputMeridiemContainer,
+} from "./TimeInput.styled";
+import { CompactInputContainer, CompactInput } from "./CompactTimeInput.styled";
+
+export type CompactTimeInputProps = BaseTimeInputProps;
+
+const CompactTimeInput = forwardRef(function TimeInput(
+  { value, is24HourMode, autoFocus, onChange }: CompactTimeInputProps,
+  ref: Ref<HTMLDivElement>,
+): JSX.Element {
+  const {
+    isAm,
+    hoursText,
+    minutesText,
+    amText,
+    pmText,
+    handleHoursChange,
+    handleMinutesChange,
+    handleAM,
+    handlePM,
+  } = useTimeInput({ value, is24HourMode, onChange });
+
+  const onAmPmChange = useCallback(
+    value => {
+      if (value === "am") {
+        handleAM();
+      } else {
+        handlePM();
+      }
+    },
+    [handleAM, handlePM],
+  );
+
+  return (
+    <InputRoot ref={ref}>
+      <CompactInputContainer>
+        <CompactInput
+          value={hoursText}
+          placeholder="00"
+          autoFocus={autoFocus}
+          fullWidth
+          aria-label={t`Hours`}
+          onChange={handleHoursChange}
+        />
+        <InputDivider>:</InputDivider>
+        <CompactInput
+          value={minutesText}
+          placeholder="00"
+          fullWidth
+          aria-label={t`Minutes`}
+          onChange={handleMinutesChange}
+        />
+      </CompactInputContainer>
+      {!is24HourMode && (
+        <InputMeridiemContainer>
+          <SegmentedControl
+            name="am-pm"
+            value={isAm ? "am" : "pm"}
+            options={[
+              { name: amText, value: "am" },
+              { name: pmText, value: "pm" },
+            ]}
+            variant="fill-all"
+            onChange={onAmPmChange}
+          />
+        </InputMeridiemContainer>
+      )}
+    </InputRoot>
+  );
+});
+
+export default CompactTimeInput;
diff --git a/frontend/src/metabase/core/components/TimeInput/TimeInput.stories.tsx b/frontend/src/metabase/core/components/TimeInput/TimeInput.stories.tsx
index d4bdf7e786ab33fb951a49f179287595d06e75eb..d778457563528e00326645541ea7669b4325e1a4 100644
--- a/frontend/src/metabase/core/components/TimeInput/TimeInput.stories.tsx
+++ b/frontend/src/metabase/core/components/TimeInput/TimeInput.stories.tsx
@@ -17,3 +17,11 @@ const Template: ComponentStory<typeof TimeInput> = args => {
 };
 
 export const Default = Template.bind({});
+
+const CompactTemplate: ComponentStory<typeof TimeInput> = args => {
+  const [value, setValue] = useState(moment("2020-01-01T10:20"));
+
+  return <TimeInput.Compact {...args} value={value} onChange={setValue} />;
+};
+
+export const Compact = CompactTemplate.bind({});
diff --git a/frontend/src/metabase/core/components/TimeInput/TimeInput.tsx b/frontend/src/metabase/core/components/TimeInput/TimeInput.tsx
index e745a41f170580245741b093238c0c2f0ec6989d..9bc9dbfb1cf81d8728cfd37ff7a3e276d9a29fe6 100644
--- a/frontend/src/metabase/core/components/TimeInput/TimeInput.tsx
+++ b/frontend/src/metabase/core/components/TimeInput/TimeInput.tsx
@@ -1,7 +1,11 @@
-import React, { forwardRef, Ref, useCallback } from "react";
+import React, { forwardRef, Ref } from "react";
 import { t } from "ttag";
-import moment, { Moment } from "moment";
+import { Moment } from "moment";
 import Tooltip from "metabase/components/Tooltip";
+
+import useTimeInput, { BaseTimeInputProps } from "./useTimeInput";
+import CompactTimeInput from "./CompactTimeInput";
+
 import {
   InputClearButton,
   InputClearIcon,
@@ -12,69 +16,35 @@ import {
   InputRoot,
 } from "./TimeInput.styled";
 
-export interface TimeInputProps {
-  value: Moment;
-  is24HourMode?: boolean;
-  autoFocus?: boolean;
-  onChange?: (value: Moment) => void;
+export interface TimeInputProps extends BaseTimeInputProps {
+  hasClearButton?: boolean;
   onClear?: (value: Moment) => void;
 }
 
 const TimeInput = forwardRef(function TimeInput(
-  { value, is24HourMode, autoFocus, onChange, onClear }: TimeInputProps,
+  {
+    value,
+    is24HourMode,
+    autoFocus,
+    hasClearButton = true,
+    onChange,
+    onClear,
+  }: TimeInputProps,
   ref: Ref<HTMLDivElement>,
 ): JSX.Element {
-  const hoursText = value.format(is24HourMode ? "HH" : "hh");
-  const minutesText = value.format("mm");
-  const isAm = value.hours() < 12;
-  const isPm = !isAm;
-  const amText = moment.localeData().meridiem(0, 0, false);
-  const pmText = moment.localeData().meridiem(12, 0, false);
-
-  const handleHoursChange = useCallback(
-    (hours = 0) => {
-      const newValue = value.clone();
-      if (is24HourMode) {
-        newValue.hours(hours % 24);
-      } else {
-        newValue.hours((hours % 12) + (isAm ? 0 : 12));
-      }
-      onChange?.(newValue);
-    },
-    [value, isAm, is24HourMode, onChange],
-  );
-
-  const handleMinutesChange = useCallback(
-    (minutes = 0) => {
-      const newValue = value.clone();
-      newValue.minutes(minutes % 60);
-      onChange?.(newValue);
-    },
-    [value, onChange],
-  );
-
-  const handleAmClick = useCallback(() => {
-    if (isPm) {
-      const newValue = value.clone();
-      newValue.hours(newValue.hours() - 12);
-      onChange?.(newValue);
-    }
-  }, [value, isPm, onChange]);
-
-  const handlePmClick = useCallback(() => {
-    if (isAm) {
-      const newValue = value.clone();
-      newValue.hours(newValue.hours() + 12);
-      onChange?.(newValue);
-    }
-  }, [value, isAm, onChange]);
-
-  const handleClearClick = useCallback(() => {
-    const newValue = value.clone();
-    newValue.hours(0);
-    newValue.minutes(0);
-    onClear?.(newValue);
-  }, [value, onClear]);
+  const {
+    isAm,
+    isPm,
+    hoursText,
+    minutesText,
+    amText,
+    pmText,
+    handleHoursChange,
+    handleMinutesChange,
+    handleAM: handleAmClick,
+    handlePM: handlePmClick,
+    handleClear: handleClearClick,
+  } = useTimeInput({ value, is24HourMode, onChange, onClear });
 
   return (
     <InputRoot ref={ref}>
@@ -104,16 +74,20 @@ const TimeInput = forwardRef(function TimeInput(
           </InputMeridiemButton>
         </InputMeridiemContainer>
       )}
-      <Tooltip tooltip={t`Remove time`}>
-        <InputClearButton
-          aria-label={t`Remove time`}
-          onClick={handleClearClick}
-        >
-          <InputClearIcon name="close" />
-        </InputClearButton>
-      </Tooltip>
+      {hasClearButton && (
+        <Tooltip tooltip={t`Remove time`}>
+          <InputClearButton
+            aria-label={t`Remove time`}
+            onClick={handleClearClick}
+          >
+            <InputClearIcon name="close" />
+          </InputClearButton>
+        </Tooltip>
+      )}
     </InputRoot>
   );
 });
 
-export default TimeInput;
+export default Object.assign(TimeInput, {
+  Compact: CompactTimeInput,
+});
diff --git a/frontend/src/metabase/core/components/TimeInput/useTimeInput.ts b/frontend/src/metabase/core/components/TimeInput/useTimeInput.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e0f01b5ca85a2220e6904d4d7e3415246621bf8
--- /dev/null
+++ b/frontend/src/metabase/core/components/TimeInput/useTimeInput.ts
@@ -0,0 +1,85 @@
+import { useCallback } from "react";
+import moment, { Moment } from "moment";
+
+export interface BaseTimeInputProps {
+  value: Moment;
+  is24HourMode?: boolean;
+  autoFocus?: boolean;
+  onChange?: (value: Moment) => void;
+  onClear?: (value: Moment) => void;
+}
+
+function useTimeInput({
+  value,
+  is24HourMode,
+  onChange,
+  onClear,
+}: BaseTimeInputProps) {
+  const hoursText = value.format(is24HourMode ? "HH" : "hh");
+  const minutesText = value.format("mm");
+  const isAm = value.hours() < 12;
+  const isPm = !isAm;
+  const amText = moment.localeData().meridiem(0, 0, false);
+  const pmText = moment.localeData().meridiem(12, 0, false);
+
+  const handleHoursChange = useCallback(
+    (hours = 0) => {
+      const newValue = value.clone();
+      if (is24HourMode) {
+        newValue.hours(hours % 24);
+      } else {
+        newValue.hours((hours % 12) + (isAm ? 0 : 12));
+      }
+      onChange?.(newValue);
+    },
+    [value, isAm, is24HourMode, onChange],
+  );
+
+  const handleMinutesChange = useCallback(
+    (minutes = 0) => {
+      const newValue = value.clone();
+      newValue.minutes(minutes % 60);
+      onChange?.(newValue);
+    },
+    [value, onChange],
+  );
+
+  const handleAM = useCallback(() => {
+    if (isPm) {
+      const newValue = value.clone();
+      newValue.hours(newValue.hours() - 12);
+      onChange?.(newValue);
+    }
+  }, [value, isPm, onChange]);
+
+  const handlePM = useCallback(() => {
+    if (isAm) {
+      const newValue = value.clone();
+      newValue.hours(newValue.hours() + 12);
+      onChange?.(newValue);
+    }
+  }, [value, isAm, onChange]);
+
+  const handleClear = useCallback(() => {
+    const newValue = value.clone();
+    newValue.hours(0);
+    newValue.minutes(0);
+    onClear?.(newValue);
+  }, [value, onClear]);
+
+  return {
+    isAm,
+    isPm,
+    hoursText,
+    minutesText,
+    amText,
+    pmText,
+    handleHoursChange,
+    handleMinutesChange,
+    handleAM,
+    handlePM,
+    handleClear,
+  };
+}
+
+export default useTimeInput;